Skip to content

Commit e57f910

Browse files
committed
Add support for server-side rendering by allowing Async to be initialized with data and not loading on mount.
1 parent 3a2ed56 commit e57f910

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ assumptions about the shape of your data or the type of request.
2121
- Automatic re-run using `watch` prop
2222
- Accepts `onResolve` and `onReject` callbacks
2323
- Supports optimistic updates using `setData`
24+
- Supports server-side rendering through `initialValue`
2425

2526
> Versions 1.x and 2.x of `react-async` on npm are from a different project abandoned years ago. The original author was
2627
> 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.
119120

120121
`<Async>` takes the following properties:
121122

122-
- `promiseFn` {() => Promise} A function that returns a promise; invoked immediately in `componentDidMount` and receives props (object) as arguments
123+
- `promiseFn` {() => Promise} A function that returns a promise; invoked immediately in `componentDidMount` and receives props (object) as argument
123124
- `deferFn` {() => Promise} A function that returns a promise; invoked only by calling `run`, with arguments being passed through
124125
- `watch` {any} Watches this property through `componentDidUpdate` and re-runs the `promiseFn` when the value changes (`oldValue !== newValue`)
126+
- `initialValue` {any} initial state for `data` or `error` (if instance of Error); useful for server-side rendering
125127
- `onResolve` {Function} Callback function invoked when a promise resolves, receives data as argument
126128
- `onReject` {Function} Callback function invoked when a promise rejects, receives error as argument
127129

@@ -131,6 +133,7 @@ Similarly, this allows you to set default `onResolve` and `onReject` callbacks.
131133

132134
- `data` {any} last resolved promise value, maintained when new error arrives
133135
- `error` {Error} rejected promise reason, cleared when new data arrives
136+
- `initialValue` {any} the data or error that was provided through the `initialValue` prop
134137
- `isLoading` {boolean} `true` while a promise is pending
135138
- `startedAt` {Date} when the current/last promise was started
136139
- `finishedAt` {Date} when the last promise was resolved or rejected
@@ -201,6 +204,36 @@ const updateAttendance = attend => fetch(...).then(() => attend, () => !attend)
201204
</Async>
202205
```
203206

207+
### Server-side rendering using `initialValue` (e.g. Next.js)
208+
209+
```js
210+
static async getInitialProps() {
211+
// Resolve the promise server-side
212+
const sessions = await loadSessions()
213+
return { sessions }
214+
}
215+
216+
render() {
217+
const { sessions } = this.props // injected by getInitialProps
218+
return (
219+
<Async promiseFn={loadSessions} initialValue={sessions}>
220+
{({ data, error, isLoading, initialValue }) => { // initialValue is passed along for convenience
221+
if (isLoading) {
222+
return <div>Loading...</div>
223+
}
224+
if (error) {
225+
return <p>{error.toString()}</p>
226+
}
227+
if (data) {
228+
return <pre>{JSON.stringify(data, null, 2)}</pre>
229+
}
230+
return null
231+
}}
232+
</Async>
233+
)
234+
}
235+
```
236+
204237
## Helper components
205238

206239
`<Async>` provides several helper components that make your JSX even more declarative.

src/index.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ export const createInstance = (defaultProps = {}) => {
1212
class Async extends React.Component {
1313
constructor(props) {
1414
super(props)
15+
16+
const promiseFn = props.promiseFn || defaultProps.promiseFn
17+
const initialValue = props.initialValue || defaultProps.initialValue
18+
const initialError = initialValue instanceof Error ? initialValue : undefined
19+
const initialData = initialError ? undefined : initialValue
20+
1521
this.mounted = false
1622
this.counter = 0
1723
this.args = []
1824
this.state = {
19-
data: undefined,
20-
error: undefined,
21-
isLoading: isFunction(props.promiseFn) || isFunction(defaultProps.promiseFn),
25+
initialValue,
26+
data: initialData,
27+
error: initialError,
28+
isLoading: !initialValue && isFunction(promiseFn),
2229
startedAt: undefined,
23-
finishedAt: undefined,
30+
finishedAt: initialValue ? new Date() : undefined,
2431
cancel: this.cancel,
2532
run: this.run,
2633
reload: () => {
@@ -34,7 +41,7 @@ export const createInstance = (defaultProps = {}) => {
3441

3542
componentDidMount() {
3643
this.mounted = true
37-
this.load()
44+
this.state.initialValue || this.load()
3845
}
3946

4047
componentDidUpdate(prevProps) {

src/spec.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,48 @@ test("cancels pending promise when unmounted", async () => {
205205
expect(onResolve).not.toHaveBeenCalled()
206206
})
207207

208+
test("does not run promiseFn on mount when initialValue is provided", () => {
209+
const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
210+
render(<Async promiseFn={promiseFn} initialValue={{}} />)
211+
expect(promiseFn).not.toHaveBeenCalled()
212+
})
213+
214+
test("does not start loading when using initialValue", async () => {
215+
const promiseFn = () => resolveTo("done")
216+
const states = []
217+
const { getByText } = render(
218+
<Async promiseFn={promiseFn} initialValue="done">
219+
{({ data, isLoading }) => {
220+
states.push(isLoading)
221+
return data
222+
}}
223+
</Async>
224+
)
225+
await waitForElement(() => getByText("done"))
226+
expect(states).toEqual([false])
227+
})
228+
229+
test("passes initialValue to children immediately", async () => {
230+
const promiseFn = () => resolveTo("done")
231+
const { getByText } = render(
232+
<Async promiseFn={promiseFn} initialValue="done">
233+
{({ data }) => data}
234+
</Async>
235+
)
236+
await waitForElement(() => getByText("done"))
237+
})
238+
239+
test("sets error instead of data when initialValue is an Error object", async () => {
240+
const promiseFn = () => resolveTo("done")
241+
const error = new Error("oops")
242+
const { getByText } = render(
243+
<Async promiseFn={promiseFn} initialValue={error}>
244+
{({ error }) => error.message}
245+
</Async>
246+
)
247+
await waitForElement(() => getByText("oops"))
248+
})
249+
208250
test("can be nested", async () => {
209251
const outerFn = () => resolveIn(0)("outer")
210252
const innerFn = () => resolveIn(100)("inner")

0 commit comments

Comments
 (0)