From e57f910ac483b1de59ba43208745d62e7817209e Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Sat, 29 Sep 2018 13:41:14 +0200 Subject: [PATCH] Add support for server-side rendering by allowing Async to be initialized with data and not loading on mount. --- README.md | 35 ++++++++++++++++++++++++++++++++++- src/index.js | 17 ++++++++++++----- src/spec.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4a43bca2..921d7646 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ assumptions about the shape of your data or the type of request. - Automatic re-run using `watch` prop - Accepts `onResolve` and `onReject` callbacks - Supports optimistic updates using `setData` +- Supports server-side rendering through `initialValue` > Versions 1.x and 2.x of `react-async` on npm are from a different project abandoned years ago. The original author was > kind enough to transfer ownership so the `react-async` package name could be repurposed. The first version of @@ -119,9 +120,10 @@ Similarly, this allows you to set default `onResolve` and `onReject` callbacks. `` takes the following properties: -- `promiseFn` {() => Promise} A function that returns a promise; invoked immediately in `componentDidMount` and receives props (object) as arguments +- `promiseFn` {() => Promise} A function that returns a promise; invoked immediately in `componentDidMount` and receives props (object) as argument - `deferFn` {() => Promise} A function that returns a promise; invoked only by calling `run`, with arguments being passed through - `watch` {any} Watches this property through `componentDidUpdate` and re-runs the `promiseFn` when the value changes (`oldValue !== newValue`) +- `initialValue` {any} initial state for `data` or `error` (if instance of Error); useful for server-side rendering - `onResolve` {Function} Callback function invoked when a promise resolves, receives data as argument - `onReject` {Function} Callback function invoked when a promise rejects, receives error as argument @@ -131,6 +133,7 @@ Similarly, this allows you to set default `onResolve` and `onReject` callbacks. - `data` {any} last resolved promise value, maintained when new error arrives - `error` {Error} rejected promise reason, cleared when new data arrives +- `initialValue` {any} the data or error that was provided through the `initialValue` prop - `isLoading` {boolean} `true` while a promise is pending - `startedAt` {Date} when the current/last promise was started - `finishedAt` {Date} when the last promise was resolved or rejected @@ -201,6 +204,36 @@ const updateAttendance = attend => fetch(...).then(() => attend, () => !attend) ``` +### Server-side rendering using `initialValue` (e.g. Next.js) + +```js +static async getInitialProps() { + // Resolve the promise server-side + const sessions = await loadSessions() + return { sessions } +} + +render() { + const { sessions } = this.props // injected by getInitialProps + return ( + + {({ data, error, isLoading, initialValue }) => { // initialValue is passed along for convenience + if (isLoading) { + return
Loading...
+ } + if (error) { + return

{error.toString()}

+ } + if (data) { + return
{JSON.stringify(data, null, 2)}
+ } + return null + }} +
+ ) +} +``` + ## Helper components `` provides several helper components that make your JSX even more declarative. diff --git a/src/index.js b/src/index.js index 243c1a64..7c743ad3 100644 --- a/src/index.js +++ b/src/index.js @@ -12,15 +12,22 @@ export const createInstance = (defaultProps = {}) => { class Async extends React.Component { constructor(props) { super(props) + + const promiseFn = props.promiseFn || defaultProps.promiseFn + const initialValue = props.initialValue || defaultProps.initialValue + const initialError = initialValue instanceof Error ? initialValue : undefined + const initialData = initialError ? undefined : initialValue + this.mounted = false this.counter = 0 this.args = [] this.state = { - data: undefined, - error: undefined, - isLoading: isFunction(props.promiseFn) || isFunction(defaultProps.promiseFn), + initialValue, + data: initialData, + error: initialError, + isLoading: !initialValue && isFunction(promiseFn), startedAt: undefined, - finishedAt: undefined, + finishedAt: initialValue ? new Date() : undefined, cancel: this.cancel, run: this.run, reload: () => { @@ -34,7 +41,7 @@ export const createInstance = (defaultProps = {}) => { componentDidMount() { this.mounted = true - this.load() + this.state.initialValue || this.load() } componentDidUpdate(prevProps) { diff --git a/src/spec.js b/src/spec.js index 65dd3707..8bd1aa69 100644 --- a/src/spec.js +++ b/src/spec.js @@ -205,6 +205,48 @@ test("cancels pending promise when unmounted", async () => { expect(onResolve).not.toHaveBeenCalled() }) +test("does not run promiseFn on mount when initialValue is provided", () => { + const promiseFn = jest.fn().mockReturnValue(Promise.resolve()) + render() + expect(promiseFn).not.toHaveBeenCalled() +}) + +test("does not start loading when using initialValue", async () => { + const promiseFn = () => resolveTo("done") + const states = [] + const { getByText } = render( + + {({ data, isLoading }) => { + states.push(isLoading) + return data + }} + + ) + await waitForElement(() => getByText("done")) + expect(states).toEqual([false]) +}) + +test("passes initialValue to children immediately", async () => { + const promiseFn = () => resolveTo("done") + const { getByText } = render( + + {({ data }) => data} + + ) + await waitForElement(() => getByText("done")) +}) + +test("sets error instead of data when initialValue is an Error object", async () => { + const promiseFn = () => resolveTo("done") + const error = new Error("oops") + const { getByText } = render( + + {({ error }) => error.message} + + ) + await waitForElement(() => getByText("oops")) +}) + test("can be nested", async () => { const outerFn = () => resolveIn(0)("outer") const innerFn = () => resolveIn(100)("inner")