Skip to content
This repository was archived by the owner on Jun 17, 2019. It is now read-only.

Commit edddd5f

Browse files
author
Josef Blake
committed
added createLoader factory
closes #4
1 parent 5cf5e45 commit edddd5f

File tree

5 files changed

+198
-12
lines changed

5 files changed

+198
-12
lines changed

README.md

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,153 @@
22

33
Async component that waits on promises to resolve!
44

5-
### Motivation
5+
## Motivation
66

77
Creating a promise is a synchronous action. If you hold a reference to a promise, you can eventually get the future value that it resolves to. Calling `setState` asynchronously in a component can cause a lot of headaches because of race conditions. The promise could still be in a pending state when the component unmounts or the props change. The `Async` component allows you to never worry about these race conditions and enables you to write your asynchronous react code as if it was synchronous.
88

9+
## Install
10+
11+
```
12+
yarn add react-async-await react
13+
```
14+
15+
## Async Component
16+
917
The `Async` component
1018

1119
* throws uncaught promise rejections so they can be handled by an [error boundary](https://reactjs.org/docs/error-boundaries.html)
1220
* injects render callback with resolved value
1321
* renders synchronously if the promise was already resolved by the Async component
1422
* prevents race conditions when props change and components unmount
1523

16-
### Install
24+
```js
25+
import React from "react";
26+
import ReactDOM from "react-dom";
27+
import { Async } from "react-async-await";
28+
29+
const getUser = id =>
30+
fetch(`/api/users/${id}`).then(response => response.json());
31+
32+
class User extends React.Component {
33+
componentWillMount() {
34+
this.setState({
35+
promise: undefined,
36+
error: undefined
37+
});
38+
}
39+
40+
componentDidMount() {
41+
this.setState({
42+
promise: getUser(this.props.id)
43+
});
44+
}
45+
46+
componentWillReceiveProps(nextProps) {
47+
if (this.props.id !== nextProps.id) {
48+
this.setState({
49+
promise: getUser(nextProps.id),
50+
error: undefined
51+
});
52+
}
53+
}
54+
55+
// If this.state.promise rejects then Async component will throw error.
56+
// Error Boundaries allow you to recover from thrown errors.
57+
// @see https://reactjs.org/docs/error-boundaries.html
58+
componentDidCatch(error) {
59+
this.setState({ error });
60+
}
61+
62+
render() {
63+
if (this.state.error) {
64+
return <div>An error occurred!</div>;
65+
}
66+
67+
return (
68+
<Async await={this.state.promise}>
69+
{user => (user ? <div>Hello {user.name}!</div> : <div>Loading...</div>)}
70+
</Async>
71+
);
72+
}
73+
}
1774

75+
ReactDOM.render(<User id={1} />, document.getElementById("root"));
1876
```
19-
yarn add react-async-await react
77+
78+
## createLoader Factory
79+
80+
Create a wrapper component around `Async` that maps props to a promise when the component mounts.
81+
82+
```js
83+
import React from "react";
84+
import ReactDOM from "react-dom";
85+
import { createLoader } from "react-async-await";
86+
87+
const getUser = id => fetch(`/api/users/${id}`).then(response => response.json());
88+
89+
const LoadUser = createLoader(
90+
props => getUser(props.id)), // loader
91+
props => props.id, // resolver
92+
);
93+
94+
class User extends React.Component {
95+
componentWillMount() {
96+
this.setState({
97+
error: undefined
98+
});
99+
}
100+
101+
componentWillReceiveProps(nextProps) {
102+
if (this.props.id !== nextProps.id) {
103+
this.setState({
104+
error: undefined
105+
});
106+
}
107+
}
108+
109+
// If this.state.promise rejects then Async component will throw error.
110+
// Error Boundaries allow you to recover from thrown errors.
111+
// @see https://reactjs.org/docs/error-boundaries.html
112+
componentDidCatch(error) {
113+
this.setState({ error });
114+
}
115+
116+
render() {
117+
if (this.state.error) {
118+
return <div>An error occurred!</div>;
119+
}
120+
121+
return (
122+
<LoadUser id={this.props.id}>
123+
{user => (user ? <div>Hello {user.name}!</div> : <div>Loading...</div>)}
124+
</LoadUser>
125+
);
126+
}
127+
}
128+
129+
ReactDOM.render(<User id={1} />, document.getElementById("root"));
20130
```
21131

22-
### Basic Usage
132+
## Caching
133+
134+
Coupling the `Async` component or `createLoader` factory with a promise cache can be extremely powerful. Instead of creating a new promise every time your component receives props, you can resolve to a cached promise by using techniques like memoization. Below is an example using [`lodash/memoize`](https://lodash.com/docs/4.17.5#memoize).
23135

24136
```js
25137
import React from "react";
26-
import { render } from "react-dom";
27-
import { Async } from "react-async-await";
138+
import ReactDOM from "react-dom";
139+
import { createLoader } from "react-async-await";
28140
import memoize from "lodash/memoize";
29141

30142
// will return same promise when passed same id
31143
// @see https://lodash.com/docs/4.17.5#memoize
32-
const getUser = memoize(id =>
33-
fetch(`/api/users/${this.props.id}`).then(response => response.json())
144+
const getUser = memoize(
145+
id => fetch(`/api/users/${id}`).then(response => response.json()),
146+
id => id,
147+
);
148+
149+
const LoadUser = createLoader(
150+
props => getUser(props.id)), // loader
151+
props => props.id, // resolver
34152
);
35153

36154
class User extends React.Component {
@@ -61,12 +179,12 @@ class User extends React.Component {
61179
}
62180

63181
return (
64-
<Async await={getUser(this.props.id)}>
182+
<LoadUser id={this.props.id}>
65183
{user => (user ? <div>Hello {user.name}!</div> : <div>Loading...</div>)}
66-
</Async>
184+
</LoadUser>
67185
);
68186
}
69187
}
70188

71-
render(<User id={1} />, document.getElementById("root"));
189+
ReactDOM.render(<User id={1} />, document.getElementById("root"));
72190
```

__tests__/createLoader.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import ReactDOM from "react-dom";
3+
import { createLoader } from "../";
4+
5+
const mountNode = document.body.appendChild(document.createElement("div"));
6+
const mount = element => ReactDOM.render(element, mountNode);
7+
8+
const Loader = createLoader(props => props.promise, props => props.promise);
9+
10+
test("renders (default)", () => {
11+
expect(() => mount(<Loader />)).not.toThrowError();
12+
});
13+
14+
test("loads promise", async () => {
15+
const a = Promise.resolve(1);
16+
const render = jest.fn(() => null);
17+
18+
mount(<Loader promise={a}>{render}</Loader>);
19+
20+
await a;
21+
22+
expect(render.mock.calls).toEqual([[], [1]]);
23+
});
24+
25+
test("resolves to new promise", async () => {
26+
const a = Promise.resolve(1);
27+
const b = Promise.resolve(2);
28+
const render = jest.fn(() => null);
29+
30+
mount(<Loader promise={a}>{render}</Loader>);
31+
mount(<Loader promise={b}>{render}</Loader>);
32+
33+
await Promise.all([a, b]);
34+
35+
expect(render.mock.calls).toEqual([[], [], [2]]);
36+
});

src/.babelrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
{
66
"modules": false
77
}
8-
]
8+
],
9+
"react"
910
],
1011
"plugins": ["external-helpers", "transform-class-properties"]
1112
}

src/createLoader.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from "react";
2+
import Async from "./Async";
3+
4+
export default function createLoader(loader, resolver) {
5+
return class Loader extends React.Component {
6+
state = {};
7+
8+
componentDidMount() {
9+
this.setState({
10+
promise: loader(this.props),
11+
promiseKey: resolver(this.props)
12+
});
13+
}
14+
15+
componentWillReceiveProps(nextProps) {
16+
const nextPromiseKey = resolver(nextProps);
17+
18+
if (this.state.promiseKey !== nextPromiseKey) {
19+
this.setState({
20+
promise: loader(nextProps),
21+
promiseKey: nextPromiseKey
22+
});
23+
}
24+
}
25+
26+
render() {
27+
return <Async await={this.state.promise}>{this.props.children}</Async>;
28+
}
29+
};
30+
}

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default as Async } from "./Async";
2+
export { default as createLoader } from "./createLoader";

0 commit comments

Comments
 (0)