diff --git a/.gitignore b/.gitignore index 481ce8d0..2e99f6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,9 @@ typings/ public lib +# Cache for parcel serverl in example +.cache + # Files managed by operational-scripts tslint.json tsconfig.json diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..b4152fc4 --- /dev/null +++ b/example/README.md @@ -0,0 +1 @@ +This is a simple live demo of `restul-react` components primarily used to test features. Run it with `npm run example`. diff --git a/example/index.html b/example/index.html new file mode 100644 index 00000000..3871f15e --- /dev/null +++ b/example/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/example/index.tsx b/example/index.tsx new file mode 100644 index 00000000..ee1c8dba --- /dev/null +++ b/example/index.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { render } from "react-dom"; + +import { Get, RestfulProvider } from "../src"; + +const wait = timeout => + new Promise((resolve, reject) => { + setTimeout(() => { + resolve(); + }, timeout); + }); + +const App: React.SFC<{}> = props => ( + + wait(1500).then(() => Promise.resolve(Object.keys(res.message)))}> + {(breeds, { loading, error }) => { + if (loading) { + return "loading.."; + } + if (error) { + return {JSON.stringify(error)}; + } + if (breeds) { + return ; + } + return null; + }} + + +); + +render(, document.querySelector("#app")); diff --git a/package.json b/package.json index 18cb0c7f..edd4dfb2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "scripts": { "start": "operational-scripts start", + "example": "parcel example/index.html", "test": "operational-scripts test", "build": "operational-scripts build --for npm", "preversion": "npm run build", @@ -54,6 +55,7 @@ "jest-dom": "^1.12.0", "lint-staged": "^7.2.0", "nock": "^10.0.0", + "parcel": "^1.10.3", "prettier": "^1.13.5", "react-dom": "^16.4.2", "react-testing-library": "^5.0.0", @@ -64,7 +66,7 @@ "typescript": "^3.0.3" }, "dependencies": { - "lodash": "^4.17.10", + "lodash": "^4.17.11", "react": "^16.4.1", "react-fast-compare": "^2.0.1", "url": "^0.11.0" diff --git a/src/Get.test.tsx b/src/Get.test.tsx index 9a538a9c..35d69477 100644 --- a/src/Get.test.tsx +++ b/src/Get.test.tsx @@ -253,6 +253,68 @@ describe("Get", () => { await wait(() => expect(children.mock.calls.length).toBe(2)); expect(children.mock.calls[1][0]).toEqual({ hello: "world", foo: "bar" }); }); + + it("should transform data with a promise", async () => { + nock("https://my-awesome-api.fake") + .get("/") + .reply(200, { hello: "world" }); + + const children = jest.fn(); + children.mockReturnValue(
); + + render( + + Promise.resolve({ ...data, foo: "bar" })}> + {children} + + , + ); + + await wait(() => expect(children.mock.calls.length).toBe(2)); + expect(children.mock.calls[1][0]).toEqual({ hello: "world", foo: "bar" }); + }); + + it("should pass an error when the resolver throws a runtime error", async () => { + nock("https://my-awesome-api.fake") + .get("/") + .reply(200, { hello: "world" }); + + const children = jest.fn(); + children.mockReturnValue(
); + + render( + + data.apples.oranges}> + {children} + + , + ); + + await wait(() => expect(children.mock.calls.length).toBe(2)); + expect(children.mock.calls[1][1].error.message).toEqual("RESOLVE_ERROR"); + expect(children.mock.calls[1][0]).toEqual(null); + }); + + it("should pass an error when the resolver is a promise that rejects", async () => { + nock("https://my-awesome-api.fake") + .get("/") + .reply(200, { hello: "world" }); + + const children = jest.fn(); + children.mockReturnValue(
); + + render( + + Promise.reject("nogood")}> + {children} + + , + ); + + await wait(() => expect(children.mock.calls.length).toBe(2)); + expect(children.mock.calls[1][1].error).toEqual({ message: "RESOLVE_ERROR", data: JSON.stringify("nogood") }); + expect(children.mock.calls[1][0]).toEqual(null); + }); }); describe("with lazy", () => { @@ -494,10 +556,8 @@ describe("Get", () => { , ), ); - await wait(() => expect(apiCalls).toEqual(1)); }); - it("should call the API only 10 times without debounce", async () => { let apiCalls = 0; nock("https://my-awesome-api.fake") @@ -505,12 +565,9 @@ describe("Get", () => { .get("/?test=XXX") .reply(200, () => ++apiCalls) .persist(); - const children = jest.fn(); children.mockReturnValue(
); - const resolve = a => a; - /** * A new instance of RestfulProvider is created on every rerender. * This will create a new resolve function every time forcing Get to @@ -524,7 +581,6 @@ describe("Get", () => { {children} , ); - times(10, i => rerender( @@ -532,7 +588,6 @@ describe("Get", () => { , ), ); - expect(apiCalls).toEqual(10); }); }); @@ -543,23 +598,19 @@ describe("Get", () => { .get("/") .reply(200, () => ++apiCalls) .persist(); - const children = jest.fn(); children.mockReturnValue(
); - const resolve = a => a; const { rerender } = render( {children} , ); - rerender( {children} , ); - expect(apiCalls).toEqual(1); }); it("should refetch when base changes", () => { @@ -567,17 +618,14 @@ describe("Get", () => { nock("https://my-awesome-api.fake") .get("/") .reply(200, () => ++apiCalls); - const children = jest.fn(); children.mockReturnValue(
); - const resolve = a => a; const { rerender } = render( {children} , ); - nock("https://my-new-api.fake") .get("/") .reply(200, () => ++apiCalls); @@ -588,7 +636,6 @@ describe("Get", () => { , ); - expect(apiCalls).toEqual(2); }); it("should refetch when path changes", () => { @@ -598,23 +645,19 @@ describe("Get", () => { .get("/?test=XXX") .reply(200, () => ++apiCalls) .persist(); - const children = jest.fn(); children.mockReturnValue(
); - const resolve = a => a; const { rerender } = render( {children} , ); - rerender( {children} , ); - expect(apiCalls).toEqual(2); }); it("should refetch when resolve changes", () => { @@ -623,17 +666,14 @@ describe("Get", () => { .get("/") .reply(200, () => ++apiCalls) .persist(); - const children = jest.fn(); children.mockReturnValue(
); - const providerResolve = a => a; const { rerender } = render( {children} , ); - const newResolve = a => a; rerender( @@ -642,7 +682,6 @@ describe("Get", () => { , ); - expect(apiCalls).toEqual(2); }); }); diff --git a/src/Get.tsx b/src/Get.tsx index c168f8d4..402d39a0 100644 --- a/src/Get.tsx +++ b/src/Get.tsx @@ -4,12 +4,13 @@ import * as React from "react"; import RestfulReactProvider, { InjectedProps, RestfulReactConsumer, RestfulReactProviderProps } from "./Context"; import { composePath, composeUrl } from "./util/composeUrl"; import { processResponse } from "./util/processResponse"; +import { resolveData } from "./util/resolveData"; /** * A function that resolves returned data from * a fetch call. */ -export type ResolveFunction = (data: any) => T; +export type ResolveFunction = ((data: any) => T) | ((data: any) => Promise); export interface GetDataError { message: string; @@ -237,7 +238,9 @@ class ContextlessGet extends React.Component< return null; } - this.setState({ loading: false, data: resolve!(data) }); + const resolved = await resolveData({ data, resolve }); + + this.setState({ loading: false, data: resolved.data, error: resolved.error }); return data; }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..0ccaaba8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,12 @@ +// Shared types across exported components and utils +// +/** + * A function that resolves returned data from + * a fetch call. + */ +export type ResolveFunction = ((data: any) => T) | ((data: any) => Promise); + +export interface GetDataError { + message: string; + data: TError | string; +} diff --git a/src/util/resolveData.ts b/src/util/resolveData.ts new file mode 100644 index 00000000..9fe3c754 --- /dev/null +++ b/src/util/resolveData.ts @@ -0,0 +1,32 @@ +import { GetDataError, ResolveFunction } from "../types"; + +export const resolveData = async ({ + data, + resolve, +}: { + data: any; + resolve?: ResolveFunction; +}): Promise<{ data: TData | null; error: GetDataError | null }> => { + let resolvedData: TData | null = null; + let resolveError: GetDataError | null = null; + try { + if (resolve) { + const resolvedDataOrPromise: TData | Promise = resolve(data); + resolvedData = (resolvedDataOrPromise as { then?: any }).then + ? ((await resolvedDataOrPromise) as TData) + : (resolvedDataOrPromise as TData); + } else { + resolvedData = data; + } + } catch (err) { + resolvedData = null; + resolveError = { + message: "RESOLVE_ERROR", + data: JSON.stringify(err), + }; + } + return { + data: resolvedData, + error: resolveError, + }; +};