Skip to content

Commit c5e1c8a

Browse files
committed
Allow creating custom instances of Async.
1 parent 03856fe commit c5e1c8a

File tree

3 files changed

+215
-162
lines changed

3 files changed

+215
-162
lines changed

README.md

+49-35
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ configure any data fetching or updates on a higher (application global) level, u
3636
data on-demand and in parallel at component level instead of in bulk at the route / page level. It's entirely decoupled
3737
from your routes, so it works well in complex applications that have a dynamic routing model or don't use routes at all.
3838

39+
`<Async>` is promise-based, so you can resolve anything you want, not just `fetch` requests.
40+
3941
## Install
4042

4143
```
@@ -51,24 +53,22 @@ import Async from "react-async"
5153

5254
const loadJson = () => fetch("/some/url").then(res => res.json())
5355

54-
const MyComponent = () => {
55-
return (
56-
<Async promiseFn={loadJson}>
57-
{({ data, error, isLoading }) => {
58-
if (isLoading) return "Loading..."
59-
if (error) return `Something went wrong: ${error.message}`
60-
if (data)
61-
return (
62-
<div>
63-
<strong>Loaded some data:</strong>
64-
<pre>{JSON.stringify(data, null, 2)}</pre>
65-
</div>
66-
)
67-
return null
68-
}}
69-
</Async>
70-
)
71-
}
56+
const MyComponent = () => (
57+
<Async promiseFn={loadJson}>
58+
{({ data, error, isLoading }) => {
59+
if (isLoading) return "Loading..."
60+
if (error) return `Something went wrong: ${error.message}`
61+
if (data)
62+
return (
63+
<div>
64+
<strong>Loaded some data:</strong>
65+
<pre>{JSON.stringify(data, null, 2)}</pre>
66+
</div>
67+
)
68+
return null
69+
}}
70+
</Async>
71+
)
7272
```
7373

7474
Using helper components (don't have to be direct children) for ease of use:
@@ -78,25 +78,39 @@ import Async from "react-async"
7878

7979
const loadJson = () => fetch("/some/url").then(res => res.json())
8080

81-
const MyComponent = () => {
82-
return (
83-
<Async promiseFn={loadJson}>
84-
<Async.Loading>Loading...</Async.Loading>
85-
<Async.Resolved>
86-
{data => (
87-
<div>
88-
<strong>Loaded some data:</strong>
89-
<pre>{JSON.stringify(data, null, 2)}</pre>
90-
</div>
91-
)}
92-
</Async.Resolved>
93-
<Async.Rejected>{error => `Something went wrong: ${error.message}`}</Async.Rejected>
94-
</Async>
95-
)
96-
}
81+
const MyComponent = () => (
82+
<Async promiseFn={loadJson}>
83+
<Async.Loading>Loading...</Async.Loading>
84+
<Async.Resolved>
85+
{data => (
86+
<div>
87+
<strong>Loaded some data:</strong>
88+
<pre>{JSON.stringify(data, null, 2)}</pre>
89+
</div>
90+
)}
91+
</Async.Resolved>
92+
<Async.Rejected>{error => `Something went wrong: ${error.message}`}</Async.Rejected>
93+
</Async>
94+
)
9795
```
9896

99-
`<Async>` is promise-based, so you can resolve anything you want, not just `fetch` requests.
97+
Creating a custom instance of Async, bound to a specific promiseFn:
98+
99+
```js
100+
import { createInstance } from 'react-async'
101+
102+
const loadCustomer = id => fetch(`/api/customers/${id}`).then(...)
103+
104+
const AsyncCustomer = createInstance({ promiseFn: loadCustomer })
105+
106+
const MyComponent = () => (
107+
<AsyncCustomer>
108+
<AsyncCustomer.Resolved>{customer => `Hello ${customer.name}`}</CustomerLoader.Resolved>
109+
</AsyncCustomer>
110+
)
111+
```
112+
113+
Similarly, this allows you to set default `onResolve` and `onReject` callbacks.
100114

101115
### Props
102116

src/index.js

+134-126
Original file line numberDiff line numberDiff line change
@@ -8,149 +8,157 @@ const getInitialState = () => ({
88
finishedAt: undefined
99
})
1010

11-
const { Consumer, Provider } = React.createContext(getInitialState())
12-
1311
/**
14-
* Renders only when promise is rejected.
15-
*
16-
* @prop {boolean} persist Show old error while loading
17-
* @prop {Function|Node} children Function (passing error and finishedAt) or React node
12+
* createInstance allows you to create instances of Async that are bound to a specific promise.
13+
* A unique instance also uses its own React context for better nesting capability.
1814
*/
19-
class Async extends React.Component {
20-
mounted = false
21-
counter = 0
22-
args = []
23-
state = getInitialState()
24-
25-
componentDidMount() {
26-
this.mounted = true
27-
this.load()
28-
}
29-
30-
componentDidUpdate(prevProps) {
31-
if (prevProps.watch !== this.props.watch) this.load()
32-
}
33-
34-
componentWillUnmount() {
35-
this.cancel()
36-
this.mounted = false
37-
}
38-
39-
load = () => {
40-
if (!this.props.promiseFn) return
41-
this.counter++
42-
this.setState({ isLoading: true, startedAt: new Date(), finishedAt: undefined })
43-
return this.props.promiseFn().then(this.onResolve(this.counter), this.onReject(this.counter))
44-
}
15+
export const createInstance = (defaultProps = {}) => {
16+
const { Consumer, Provider } = React.createContext(getInitialState())
17+
18+
class Async extends React.Component {
19+
mounted = false
20+
counter = 0
21+
args = []
22+
state = getInitialState()
23+
24+
componentDidMount() {
25+
this.mounted = true
26+
this.load()
27+
}
4528

46-
run = (...args) => {
47-
if (!this.props.deferFn) return
48-
this.counter++
49-
this.args = args
50-
this.setState({ isLoading: true, startedAt: new Date(), finishedAt: undefined })
51-
return this.props.deferFn(...args).then(this.onResolve(this.counter), this.onReject(this.counter))
52-
}
29+
componentDidUpdate(prevProps) {
30+
if (prevProps.watch !== this.props.watch) this.load()
31+
}
5332

54-
cancel = () => {
55-
this.counter++
56-
this.setState({ isLoading: false, startedAt: undefined })
57-
}
33+
componentWillUnmount() {
34+
this.cancel()
35+
this.mounted = false
36+
}
5837

59-
onResolve = counter => data => {
60-
if (this.mounted && this.counter === counter) {
61-
this.setData(data, () => this.props.onResolve && this.props.onResolve(data))
38+
load = () => {
39+
const promiseFn = this.props.promiseFn || defaultProps.promiseFn
40+
if (!promiseFn) return
41+
this.counter++
42+
this.setState({ isLoading: true, startedAt: new Date(), finishedAt: undefined })
43+
return promiseFn().then(this.onResolve(this.counter), this.onReject(this.counter))
6244
}
63-
return data
64-
}
6545

66-
onReject = counter => error => {
67-
if (this.mounted && this.counter === counter) {
68-
this.setError(error, () => this.props.onReject && this.props.onReject(error))
46+
run = (...args) => {
47+
const deferFn = this.props.deferFn || defaultProps.deferFn
48+
if (!deferFn) return
49+
this.counter++
50+
this.args = args
51+
this.setState({ isLoading: true, startedAt: new Date(), finishedAt: undefined })
52+
return deferFn(...args).then(this.onResolve(this.counter), this.onReject(this.counter))
6953
}
70-
return error
71-
}
7254

73-
setData = (data, callback) => {
74-
this.setState({ data, error: undefined, isLoading: false, finishedAt: new Date() }, callback)
75-
return data
76-
}
55+
cancel = () => {
56+
this.counter++
57+
this.setState({ isLoading: false, startedAt: undefined })
58+
}
7759

78-
setError = (error, callback) => {
79-
this.setState({ error, isLoading: false, finishedAt: new Date() }, callback)
80-
return error
81-
}
60+
onResolve = counter => data => {
61+
if (this.mounted && this.counter === counter) {
62+
const onResolve = this.props.onResolve || defaultProps.onResolve
63+
this.setData(data, () => onResolve && onResolve(data))
64+
}
65+
return data
66+
}
8267

83-
render() {
84-
const renderProps = {
85-
...this.state,
86-
cancel: this.cancel,
87-
run: this.run,
88-
reload: () => {
89-
this.load()
90-
this.run(...this.args)
91-
},
92-
setData: this.setData,
93-
setError: this.setError
68+
onReject = counter => error => {
69+
if (this.mounted && this.counter === counter) {
70+
const onReject = this.props.onReject || defaultProps.onReject
71+
this.setError(error, () => onReject && onReject(error))
72+
}
73+
return error
9474
}
9575

96-
if (typeof this.props.children === "function") {
97-
return this.props.children(renderProps)
76+
setData = (data, callback) => {
77+
this.setState({ data, error: undefined, isLoading: false, finishedAt: new Date() }, callback)
78+
return data
9879
}
9980

100-
if (this.props.children) {
101-
return <Provider value={renderProps}>{this.props.children}</Provider>
81+
setError = (error, callback) => {
82+
this.setState({ error, isLoading: false, finishedAt: new Date() }, callback)
83+
return error
10284
}
10385

104-
return null
86+
render() {
87+
const { children } = this.props
88+
89+
const renderProps = {
90+
...this.state,
91+
cancel: this.cancel,
92+
run: this.run,
93+
reload: () => {
94+
this.load()
95+
this.run(...this.args)
96+
},
97+
setData: this.setData,
98+
setError: this.setError
99+
}
100+
101+
if (typeof children === "function") {
102+
return <Provider value={renderProps}>{children(renderProps)}</Provider>
103+
}
104+
105+
if (children !== undefined && children !== null) {
106+
return <Provider value={renderProps}>{children}</Provider>
107+
}
108+
109+
return null
110+
}
105111
}
106-
}
107-
108-
/**
109-
* Renders only while loading.
110-
*
111-
* @prop {boolean} initial Show only on initial load (data is undefined)
112-
* @prop {Function|Node} children Function (passing props) or React node
113-
*/
114-
Async.Loading = ({ children, initial }) => (
115-
<Consumer>
116-
{props => {
117-
if (!props.isLoading) return null
118-
if (initial && props.data !== undefined) return null
119-
return typeof children === "function" ? children(props) : children || null
120-
}}
121-
</Consumer>
122-
)
123112

124-
/**
125-
* Renders only when promise is resolved.
126-
*
127-
* @prop {boolean} persist Show old data while loading
128-
* @prop {Function|Node} children Function (passing data and props) or React node
129-
*/
130-
Async.Resolved = ({ children, persist }) => (
131-
<Consumer>
132-
{props => {
133-
if (props.data === undefined) return null
134-
if (props.isLoading && !persist) return null
135-
return typeof children === "function" ? children(props.data, props) : children || null
136-
}}
137-
</Consumer>
138-
)
113+
/**
114+
* Renders only while loading.
115+
*
116+
* @prop {boolean} initial Show only on initial load (data is undefined)
117+
* @prop {Function|Node} children Function (passing props) or React node
118+
*/
119+
Async.Loading = ({ children, initial }) => (
120+
<Consumer>
121+
{props => {
122+
if (!props.isLoading) return null
123+
if (initial && props.data !== undefined) return null
124+
return typeof children === "function" ? children(props) : children || null
125+
}}
126+
</Consumer>
127+
)
128+
129+
/**
130+
* Renders only when promise is resolved.
131+
*
132+
* @prop {boolean} persist Show old data while loading
133+
* @prop {Function|Node} children Function (passing data and props) or React node
134+
*/
135+
Async.Resolved = ({ children, persist }) => (
136+
<Consumer>
137+
{props => {
138+
if (props.data === undefined) return null
139+
if (props.isLoading && !persist) return null
140+
return typeof children === "function" ? children(props.data, props) : children || null
141+
}}
142+
</Consumer>
143+
)
144+
145+
/**
146+
* Renders only when promise is rejected.
147+
*
148+
* @prop {boolean} persist Show old error while loading
149+
* @prop {Function|Node} children Function (passing error and props) or React node
150+
*/
151+
Async.Rejected = ({ children, persist }) => (
152+
<Consumer>
153+
{props => {
154+
if (props.error === undefined) return null
155+
if (props.isLoading && !persist) return null
156+
return typeof children === "function" ? children(props.error, props) : children || null
157+
}}
158+
</Consumer>
159+
)
160+
161+
return Async
162+
}
139163

140-
/**
141-
* Renders only when promise is rejected.
142-
*
143-
* @prop {boolean} persist Show old error while loading
144-
* @prop {Function|Node} children Function (passing error and props) or React node
145-
*/
146-
Async.Rejected = ({ children, persist }) => (
147-
<Consumer>
148-
{props => {
149-
if (props.error === undefined) return null
150-
if (props.isLoading && !persist) return null
151-
return typeof children === "function" ? children(props.error, props) : children || null
152-
}}
153-
</Consumer>
154-
)
155-
156-
export default Async
164+
export default createInstance()

0 commit comments

Comments
 (0)