Skip to content

Commit

Permalink
Add support for server-side rendering by allowing Async to be initial…
Browse files Browse the repository at this point in the history
…ized with data and not loading on mount.
  • Loading branch information
ghengeveld committed Sep 29, 2018
1 parent 3a2ed56 commit e57f910
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 6 deletions.
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -119,9 +120,10 @@ Similarly, this allows you to set default `onResolve` and `onReject` callbacks.

`<Async>` 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

Expand All @@ -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
Expand Down Expand Up @@ -201,6 +204,36 @@ const updateAttendance = attend => fetch(...).then(() => attend, () => !attend)
</Async>
```

### 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 (
<Async promiseFn={loadSessions} initialValue={sessions}>
{({ data, error, isLoading, initialValue }) => { // initialValue is passed along for convenience
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <p>{error.toString()}</p>
}
if (data) {
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
return null
}}
</Async>
)
}
```

## Helper components

`<Async>` provides several helper components that make your JSX even more declarative.
Expand Down
17 changes: 12 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand All @@ -34,7 +41,7 @@ export const createInstance = (defaultProps = {}) => {

componentDidMount() {
this.mounted = true
this.load()
this.state.initialValue || this.load()
}

componentDidUpdate(prevProps) {
Expand Down
42 changes: 42 additions & 0 deletions src/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Async promiseFn={promiseFn} initialValue={{}} />)
expect(promiseFn).not.toHaveBeenCalled()
})

test("does not start loading when using initialValue", async () => {
const promiseFn = () => resolveTo("done")
const states = []
const { getByText } = render(
<Async promiseFn={promiseFn} initialValue="done">
{({ data, isLoading }) => {
states.push(isLoading)
return data
}}
</Async>
)
await waitForElement(() => getByText("done"))
expect(states).toEqual([false])
})

test("passes initialValue to children immediately", async () => {
const promiseFn = () => resolveTo("done")
const { getByText } = render(
<Async promiseFn={promiseFn} initialValue="done">
{({ data }) => data}
</Async>
)
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(
<Async promiseFn={promiseFn} initialValue={error}>
{({ error }) => error.message}
</Async>
)
await waitForElement(() => getByText("oops"))
})

test("can be nested", async () => {
const outerFn = () => resolveIn(0)("outer")
const innerFn = () => resolveIn(100)("inner")
Expand Down

0 comments on commit e57f910

Please sign in to comment.