Skip to content

Commit

Permalink
Added onRequest & onResponse in RestfulProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
ahansaary authored and fabien0102 committed Apr 12, 2020
1 parent 6ccf651 commit b00c0b1
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 10 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ export interface RestfulReactProviderProps<T = any> {
* to deal with your retry locally instead of in the provider scope.
*/
onError?: (err: any, retry: () => Promise<T | null>, response?: Response) => void;
/**
* Trigger on each request.
*/
onRequest?: (req: Request) => void;
/**
* Trigger on each response.
*/
onResponse?: (req: Response) => void;
}

// Usage
Expand Down
14 changes: 14 additions & 0 deletions src/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ export interface RestfulReactProviderProps<TData = any> {
retry: () => Promise<TData | null>,
response?: Response,
) => void;
/**
* Trigger on each request
*/
onRequest?: (req: Request) => void;
/**
* Trigger on each response
*/
onResponse?: (res: Response) => void;
/**
* Any global level query params?
* **Warning:** it's probably not a good idea to put API keys here. Consider headers instead.
Expand All @@ -48,11 +56,15 @@ export const Context = React.createContext<Required<RestfulReactProviderProps>>(
resolve: (data: any) => data,
requestOptions: {},
onError: noop,
onRequest: noop,
onResponse: noop,
queryParams: {},
});

export interface InjectedProps {
onError: RestfulReactProviderProps["onError"];
onRequest: RestfulReactProviderProps["onRequest"];
onResponse: RestfulReactProviderProps["onResponse"];
}

export default class RestfulReactProvider<T> extends React.Component<RestfulReactProviderProps<T>> {
Expand All @@ -64,6 +76,8 @@ export default class RestfulReactProvider<T> extends React.Component<RestfulReac
<Context.Provider
value={{
onError: noop,
onRequest: noop,
onResponse: noop,
resolve: (data: any) => data,
requestOptions: {},
parentPath: "",
Expand Down
43 changes: 43 additions & 0 deletions src/Get.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,49 @@ describe("Get", () => {
requestResolves!();
await wait(() => expect(resolve).not.toHaveBeenCalled());
});

it("should call the provider onRequest", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onRequest = jest.fn();
const request = new Request(path);

render(
<RestfulProvider base="https://my-awesome-api.fake" onRequest={onRequest}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(onRequest).toBeCalledWith(request);
});

it("should call the provider onResponse", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onResponse = jest.fn();

render(
<RestfulProvider base="https://my-awesome-api.fake" onResponse={onResponse}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(onResponse).toBeCalled();
});
});

describe("with error", () => {
Expand Down
9 changes: 6 additions & 3 deletions src/Get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ class ContextlessGet<TData, TError, TQueryParams> extends React.Component<
};

public fetch = async (requestPath?: string, thisRequestOptions?: RequestInit) => {
const { base, __internal_hasExplicitBase, parentPath, path, resolve } = this.props;
const { base, __internal_hasExplicitBase, parentPath, path, resolve, onError, onRequest, onResponse } = this.props;

if (this.state.error || !this.state.loading) {
this.setState(() => ({ error: null, loading: true }));
}
Expand All @@ -256,9 +257,11 @@ class ContextlessGet<TData, TError, TQueryParams> extends React.Component<
};

const request = new Request(makeRequestPath(), await this.getRequestOptions(thisRequestOptions));
if (onRequest) onRequest(request);
try {
const response = await fetch(request, { signal: this.signal });
const { data, responseError } = await processResponse(response);
if (onResponse) onResponse(response);

// avoid state updates when component has been unmounted
if (this.signal.aborted) {
Expand All @@ -277,8 +280,8 @@ class ContextlessGet<TData, TError, TQueryParams> extends React.Component<
error,
});

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error, () => this.fetch(requestPath, thisRequestOptions), response);
if (!this.props.localErrorOnly && onError) {
onError(error, () => this.fetch(requestPath, thisRequestOptions), response);
}

return null;
Expand Down
78 changes: 78 additions & 0 deletions src/Mutate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -902,5 +902,83 @@ describe("Mutate", () => {
// after post state
expect(children.mock.calls[2][1].loading).toEqual(false);
});

it("should call the provider onRequest", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.post("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onRequest = jest.fn();
const request = new Request(path, {
method: "POST",
headers: { "content-type": "text/plain" },
});

render(
<RestfulProvider base="https://my-awesome-api.fake" onRequest={onRequest}>
<Mutate<void, void, void> verb="POST" path="">
{children}
</Mutate>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(1));
expect(children.mock.calls[0][1].loading).toEqual(false);
expect(children.mock.calls[0][0]).toBeDefined();

// post action
children.mock.calls[0][0]();
await wait(() => expect(children.mock.calls.length).toBe(3));

// transition state
expect(children.mock.calls[1][1].loading).toEqual(true);

// after post state
expect(children.mock.calls[2][1].loading).toEqual(false);

// expect onRequest to be called
expect(onRequest).toBeCalledWith(request);
});

it("should call the provider onResponse", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.post("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onResponse = jest.fn();

render(
<RestfulProvider base="https://my-awesome-api.fake" onResponse={onResponse}>
<Mutate<void, void, void> verb="POST" path="">
{children}
</Mutate>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(1));
expect(children.mock.calls[0][1].loading).toEqual(false);
expect(children.mock.calls[0][0]).toBeDefined();

// post action
children.mock.calls[0][0]();
await wait(() => expect(children.mock.calls.length).toBe(3));

// transition state
expect(children.mock.calls[1][1].loading).toEqual(true);

// after post state
expect(children.mock.calls[2][1].loading).toEqual(false);

// expect onResponse to be called
expect(onResponse).toBeCalled();
});
});
});
13 changes: 9 additions & 4 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ class ContextlessMutate<TData, TError, TQueryParams, TRequestBody> extends React
path,
verb,
requestOptions: providerRequestOptions,
onError,
onRequest,
onResponse,
} = this.props;
this.setState(() => ({ error: null, loading: true }));

Expand Down Expand Up @@ -171,10 +174,12 @@ class ContextlessMutate<TData, TError, TQueryParams, TRequestBody> extends React
...(mutateRequestOptions ? mutateRequestOptions.headers : {}),
},
} as RequestInit); // Type assertion for version of TypeScript that can't yet discriminate.
if (onRequest) onRequest(request);

let response: Response;
try {
response = await fetch(request, { signal: this.signal });
if (onResponse) onResponse(response);
} catch (e) {
const error = {
message: `Failed to fetch: ${e.message}`,
Expand All @@ -186,8 +191,8 @@ class ContextlessMutate<TData, TError, TQueryParams, TRequestBody> extends React
loading: false,
});

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error, () => this.mutate(body, mutateRequestOptions));
if (!this.props.localErrorOnly && onError) {
onError(error, () => this.mutate(body, mutateRequestOptions));
}

throw error;
Expand All @@ -211,8 +216,8 @@ class ContextlessMutate<TData, TError, TQueryParams, TRequestBody> extends React
loading: false,
});

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error, () => this.mutate(body, mutateRequestOptions), response);
if (!this.props.localErrorOnly && onError) {
onError(error, () => this.mutate(body, mutateRequestOptions), response);
}

throw error;
Expand Down
43 changes: 43 additions & 0 deletions src/Poll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,49 @@ describe("Poll", () => {

await wait(() => expect(children.mock.calls.length).toBe(2));
});

it("should call the provider onRequest", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onRequest = jest.fn();
const request = new Request(path, { headers: { prefer: "wait=60s;" } });

render(
<RestfulProvider base="https://my-awesome-api.fake" onRequest={onRequest}>
<Poll path="">{children}</Poll>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(1));
expect(onRequest).toBeCalledWith(request);
});

it("should call the provider onResponse", async () => {
const path = "https://my-awesome-api.fake";
nock(path)
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

const onResponse = jest.fn();

render(
<RestfulProvider base="https://my-awesome-api.fake" onResponse={onResponse}>
<Poll path="">{children}</Poll>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(onResponse).toBeCalled();
});
});

describe("with error", () => {
Expand Down
8 changes: 5 additions & 3 deletions src/Poll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class ContextlessPoll<TData, TError, TQueryParams> extends React.Component<
}

// If we should keep going,
const { base, path, interval, wait } = this.props;
const { base, path, interval, wait, onError, onRequest, onResponse } = this.props;
const { lastPollIndex } = this.state;
const requestOptions = await this.getRequestOptions();

Expand All @@ -235,10 +235,12 @@ class ContextlessPoll<TData, TError, TQueryParams> extends React.Component<
...requestOptions.headers,
},
});
if (onRequest) onRequest(request);

try {
const response = await fetch(request, { signal: this.signal });
const { data, responseError } = await processResponse(response);
if (onResponse) onResponse(response);

if (!this.keepPolling || this.signal.aborted) {
// Early return if we have stopped polling or component was unmounted
Expand All @@ -254,8 +256,8 @@ class ContextlessPoll<TData, TError, TQueryParams> extends React.Component<
};
this.setState({ loading: false, lastResponse: response, error });

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error, () => Promise.resolve(), response);
if (!this.props.localErrorOnly && onError) {
onError(error, () => Promise.resolve(), response);
}
} else if (this.isModified(response, data)) {
this.setState(prevState => ({
Expand Down
Loading

0 comments on commit b00c0b1

Please sign in to comment.