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 {breeds.map((breed, breedIndex) => - {breed}
)}
;
+ }
+ 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,
+ };
+};