Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Add a provider error handler
Browse files Browse the repository at this point in the history
  • Loading branch information
fabien0102 committed Sep 13, 2018
1 parent e5e7a5f commit 871182e
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 15 deletions.
25 changes: 23 additions & 2 deletions src/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import noop from "lodash/noop";
import * as React from "react";
import { ResolveFunction } from "./Get";

Expand All @@ -13,18 +14,38 @@ export interface RestfulReactProviderProps<T = any> {
* Options passed to the fetch request.
*/
requestOptions?: (() => Partial<RequestInit>) | Partial<RequestInit>;
/**
* Trigger on each error
*/
onError?: (err: any) => void;
}

const { Provider, Consumer: RestfulReactConsumer } = React.createContext<RestfulReactProviderProps>({
const { Provider, Consumer: RestfulReactConsumer } = React.createContext<Required<RestfulReactProviderProps>>({
base: "",
resolve: (data: any) => data,
requestOptions: {},
onError: noop,
});

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

export default class RestfulReactProvider<T> extends React.Component<RestfulReactProviderProps<T>> {
public render() {
const { children, ...value } = this.props;
return <Provider value={value}>{children}</Provider>;
return (
<Provider
value={{
onError: noop,
resolve: (data: any) => data,
requestOptions: {},
...value,
}}
>
{children}
</Provider>
);
}
}

Expand Down
49 changes: 48 additions & 1 deletion src/Get.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ describe("Get", () => {
it("should deal with non standard server error response (nginx style)", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, "<html>404 - this is not a json!</html>", { "content-type": "application/json" });
.reply(200, "<html>404 - this is not a json!</html>", {
"content-type": "application/json",
});

const children = jest.fn();
children.mockReturnValue(<div />);
Expand All @@ -149,6 +151,51 @@ describe("Get", () => {
"Failed to fetch: 200 OK - invalid json response body at https://my-awesome-api.fake reason: Unexpected token < in JSON at position 0",
});
});

it("should call the provider onError", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(onError).toBeCalledWith({
data: { message: "You shall not pass!" },
message: "Failed to fetch: 401 Unauthorized",
});
});

it("should not call the provider onError if localErrorOnly is true", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

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

describe("with custom resolver", () => {
Expand Down
25 changes: 18 additions & 7 deletions src/Get.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DebounceSettings } from "lodash";
import debounce from "lodash/debounce";
import * as React from "react";
import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import RestfulReactProvider, { InjectedProps, RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import { processResponse } from "./util/processResponse";

/**
Expand Down Expand Up @@ -65,6 +65,10 @@ export interface GetProps<TData, TError> {
children: (data: TData | null, states: States<TData, TError>, actions: Actions<TData>, meta: Meta) => React.ReactNode;
/** Options passed into the fetch call. */
requestOptions?: RestfulReactProviderProps["requestOptions"];
/**
* Don't send the error to the Provider
*/
localErrorOnly?: boolean;
/**
* A function to resolve data return from the backend, most typically
* used when the backend response needs to be adapted in some way.
Expand Down Expand Up @@ -117,10 +121,10 @@ export interface GetState<TData, TError> {
* debugging.
*/
class ContextlessGet<TData, TError> extends React.Component<
GetProps<TData, TError>,
GetProps<TData, TError> & InjectedProps,
Readonly<GetState<TData, TError>>
> {
constructor(props: GetProps<TData, TError>) {
constructor(props: GetProps<TData, TError> & InjectedProps) {
super(props);

if (typeof props.debounce === "object") {
Expand Down Expand Up @@ -199,13 +203,20 @@ class ContextlessGet<TData, TError> extends React.Component<
const { data, responseError } = await processResponse(response);

if (!response.ok || responseError) {
const error = {
message: `Failed to fetch: ${response.status} ${response.statusText}${responseError ? " - " + data : ""}`,
data,
};

this.setState({
loading: false,
error: {
message: `Failed to fetch: ${response.status} ${response.statusText}${responseError ? " - " + data : ""}`,
data,
},
error,
});

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error);
}

return null;
}

Expand Down
55 changes: 55 additions & 0 deletions src/Mutate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,60 @@ describe("Mutate", () => {
});
});
});

it("should call the provider onError", async () => {
nock("https://my-awesome-api.fake")
.post("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

// post action
await children.mock.calls[0][0]().catch(() => {
/* noop */
});

expect(onError).toBeCalledWith({
data: { message: "You shall not pass!" },
message: "Failed to fetch: 401 Unauthorized",
});
});

it("should not call the provider onError if localErrorOnly is true", async () => {
nock("https://my-awesome-api.fake")
.post("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

// post action
await children.mock.calls[0][0]().catch(() => {
/* noop */
});

expect(onError.mock.calls.length).toEqual(0);
});
});
});
20 changes: 17 additions & 3 deletions src/Mutate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import RestfulReactProvider, { InjectedProps, RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import { GetState } from "./Get";
import { processResponse } from "./util/processResponse";

Expand Down Expand Up @@ -46,6 +46,10 @@ export interface MutateCommonProps {
base?: string;
/** Options passed into the fetch call. */
requestOptions?: RestfulReactProviderProps["requestOptions"];
/**
* Don't send the error to the Provider
*/
localErrorOnly?: boolean;
}

export interface MutateWithDeleteProps<TData, TError> extends MutateCommonProps {
Expand Down Expand Up @@ -96,7 +100,10 @@ export interface MutateState<TData, TError> {
* is a named class because it is useful in
* debugging.
*/
class ContextlessMutate<TData, TError> extends React.Component<MutateProps<TData, TError>, MutateState<TData, TError>> {
class ContextlessMutate<TData, TError> extends React.Component<
MutateProps<TData, TError> & InjectedProps,
MutateState<TData, TError>
> {
public readonly state: Readonly<MutateState<TData, TError>> = {
response: null,
loading: false,
Expand Down Expand Up @@ -126,10 +133,17 @@ class ContextlessMutate<TData, TError> extends React.Component<MutateProps<TData
const { data, responseError } = await processResponse(response);

if (!response.ok || responseError) {
const error = { data, message: `Failed to fetch: ${response.status} ${response.statusText}` };

this.setState({
loading: false,
});
throw { data, message: `Failed to fetch: ${response.status} ${response.statusText}` };

if (!this.props.localErrorOnly && this.props.onError) {
this.props.onError(error);
}

throw error;
}

this.setState({ loading: false });
Expand Down
45 changes: 45 additions & 0 deletions src/Poll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,51 @@ describe("Poll", () => {
expect(children.mock.calls[2][0]).toEqual({ data: "hello" });
expect(children.mock.calls[2][1].error).toEqual(null);
});

it("should call the provider onError", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(onError).toBeCalledWith({
data: { message: "You shall not pass!" },
message: "Failed to poll: 401 Unauthorized",
});
});

it("should set the `error` object properly", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(401, { message: "You shall not pass!" });

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

const onError = jest.fn();

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

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

describe("with custom resolver", () => {
Expand Down
12 changes: 10 additions & 2 deletions src/Poll.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import equal from "react-fast-compare";

import { RestfulReactConsumer } from "./Context";
import { InjectedProps, RestfulReactConsumer } from "./Context";
import { GetProps, GetState, Meta as GetComponentMeta } from "./Get";
import { processResponse } from "./util/processResponse";

Expand Down Expand Up @@ -99,6 +99,10 @@ export interface PollProps<TData, TError> {
* Any options to be passed to this request.
*/
requestOptions?: GetProps<TData, TError>["requestOptions"];
/**
* Don't send the error to the Provider
*/
localErrorOnly?: boolean;
}

/**
Expand Down Expand Up @@ -141,7 +145,7 @@ export interface PollState<TData, TError> {
* The <Poll /> component without context.
*/
class ContextlessPoll<TData, TError> extends React.Component<
PollProps<TData, TError>,
PollProps<TData, TError> & InjectedProps,
Readonly<PollState<TData, TError>>
> {
public readonly state: Readonly<PollState<TData, TError>> = {
Expand Down Expand Up @@ -226,6 +230,10 @@ class ContextlessPoll<TData, TError> extends React.Component<
data,
};
this.setState({ loading: false, lastResponse: response, error });

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

0 comments on commit 871182e

Please sign in to comment.