Skip to content

Commit 57d2143

Browse files
committed
improvement: deferFn and promiseFn now have the same signature.
The `promiseFn` and the `deferFn` have been unified. They now share the following signature: ```ts export type AsyncFn<T, C> = ( context: C | undefined, props: AsyncProps<T, C>, controller: AbortController ) => Promise<T> ``` Before the `deferFn` and `promiseFn` had this signature: ```ts export type PromiseFn<T> = (props: AsyncProps<T>, controller: AbortController) => Promise<T> export type DeferFn<T> = ( args: any[], props: AsyncProps<T>, controller: AbortController ) => Promise<T> ``` The big change is the introduction of the `context` parameter. The idea behind this parameter is that it will contain the parameters which are not known to `AsyncOptions` for use in the `promiseFn` and `asyncFn`. Another goal of this commit is to make TypeScript more understanding of the `context` which `AsyncProps` implicitly carries around. Before this commit the `AsyncProps` accepted extra prop via `[prop: string]: any`. This breaks TypeScript's understanding of the divisions somewhat. This also led to missing types for `onCancel` and `suspense`, which have been added in this commit. To solve this we no longer allow random extra properties that are unknown to `AsyncProps`. Instead only the new `context` of `AsyncProps` is passed. This means that the `[prop: string]: any` of `AsyncProps` is removed this makes TypeScript understand the props better. This is of course a breaking change. Also now compiling TypeScript on `yarn test` this should prevent type errors from slipping in. Closes: #246
1 parent 2664df0 commit 57d2143

File tree

30 files changed

+404
-138
lines changed

30 files changed

+404
-138
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ lerna-debug.log*
1515
# when working with contributors
1616
package-lock.json
1717
yarn.lock
18+
19+
.vscode

docs/api/options.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ These can be passed in an object to `useAsync(options)`, or as props to `<Async
55
- [`promise`](#promise) An already started Promise instance.
66
- [`promiseFn`](#promisefn) Function that returns a Promise, automatically invoked.
77
- [`deferFn`](#deferfn) Function that returns a Promise, manually invoked with `run`.
8+
- [`context`](#context) The first argument for the `promise` and `promiseFn` function.
89
- [`watch`](#watch) Watch a value and automatically reload when it changes.
910
- [`watchFn`](#watchfn) Watch this function and automatically reload when it returns truthy.
1011
- [`initialValue`](#initialvalue) Provide initial data or error for server-side rendering.
@@ -31,17 +32,17 @@ A Promise instance which has already started. It will simply add the necessary r
3132
3233
## `promiseFn`
3334

34-
> `function(props: Object, controller: AbortController): Promise`
35+
> `function(context C, props: AsyncOptions, controller: AbortController): Promise`
3536
3637
A function that returns a promise. It is automatically invoked in `componentDidMount` and `componentDidUpdate`. The function receives all component props \(or options\) and an AbortController instance as arguments.
3738

38-
> Be aware that updating `promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the `promiseFn` value **outside** of the render method. If you need to pass variables to the `promiseFn`, pass them as additional props to `<Async>`, as `promiseFn` will be invoked with these props. Alternatively you can use `useCallback` or [memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates.
39+
> Be aware that updating `promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the `promiseFn` value **outside** of the render method. If you need to pass variables to the `promiseFn`, pass them via the `context` props of `<Async>`, as `promiseFn` will be invoked with these props. Alternatively you can use `useCallback` or [memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates.
3940
4041
## `deferFn`
4142

42-
> `function(args: any[], props: Object, controller: AbortController): Promise`
43+
> `function(context: C, props: AsyncOptions, controller: AbortController): Promise`
4344
44-
A function that returns a promise. This is invoked only by manually calling `run(...args)`. Receives the same arguments as `promiseFn`, as well as any arguments to `run` which are passed through as an array. The `deferFn` is commonly used to send data to the server following a user action, such as submitting a form. You can use this in conjunction with `promiseFn` to fill the form with existing data, then updating it on submit with `deferFn`.
45+
A function that returns a promise. This is invoked only by manually calling `run(param)`. Receives the same arguments as `promiseFn`. The `deferFn` is commonly used to send data to the server following a user action, such as submitting a form. You can use this in conjunction with `promiseFn` to fill the form with existing data, then updating it on submit with `deferFn`.
4546

4647
> Be aware that when using both `promiseFn` and `deferFn`, the shape of their fulfilled value should match, because they both update the same `data`.
4748
@@ -132,3 +133,10 @@ Enables the use of `deferFn` if `true`, or enables the use of `promiseFn` if `fa
132133
> `boolean`
133134
134135
Enables or disables JSON parsing of the response body. By default this is automatically enabled if the `Accept` header is set to `"application/json"`.
136+
137+
138+
## `context`
139+
140+
> `C | undefined`
141+
142+
The argument which is passed as the first argument to the `promiseFn` and the `deferFn`.

docs/getting-started/upgrading.md

+182
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,187 @@
11
# Upgrading
22

3+
## Upgrade to v11
4+
5+
The `promiseFn` and the `deferFn` have been unified. They now share the following signature:
6+
7+
```ts
8+
export type AsyncFn<T, C> = (
9+
context: C | undefined,
10+
props: AsyncProps<T, C>,
11+
controller: AbortController
12+
) => Promise<T>
13+
```
14+
15+
Before the `deferFn` and `promiseFn` had this signature:
16+
17+
```ts
18+
export type PromiseFn<T> = (props: AsyncProps<T>, controller: AbortController) => Promise<T>
19+
20+
export type DeferFn<T> = (
21+
args: any[],
22+
props: AsyncProps<T>,
23+
controller: AbortController
24+
) => Promise<T>
25+
```
26+
27+
The difference is the idea of having a `context`, the context will contain all parameters
28+
to `AsyncProps` which are not native to the `AsyncProps`. Before you could pass any parameter
29+
to `AsyncProps` and it would pass them to the `deferFn` and `promiseFn`, now you need to use
30+
the `context` instead.
31+
32+
For example before you could write:
33+
34+
```jsx
35+
useAsync({ promiseFn: loadPlayer, playerId: 1 })
36+
```
37+
38+
Now you must write:
39+
40+
```jsx
41+
useAsync({ promiseFn: loadPlayer, context: { playerId: 1 }})
42+
```
43+
44+
In the above example the context would be `{playerId: 1}`.
45+
46+
This means that `promiseFn` now expects three parameters instead of two.
47+
48+
So before in `< 10.0.0` you would do this:
49+
50+
```jsx
51+
import { useAsync } from "react-async"
52+
53+
// Here loadPlayer has only two arguments
54+
const loadPlayer = async (options, controller) => {
55+
const res = await fetch(`/api/players/${options.playerId}`, { signal: controller.signal })
56+
if (!res.ok) throw new Error(res.statusText)
57+
return res.json()
58+
}
59+
60+
// With hooks
61+
const MyComponent = () => {
62+
const state = useAsync({ promiseFn: loadPlayer, playerId: 1 })
63+
}
64+
65+
// With the Async component
66+
<Async promiseFn={loadPlayer} playerId={1} />
67+
```
68+
69+
In `11.0.0` you need to account for the three parameters:
70+
71+
```jsx
72+
import { useAsync } from "react-async"
73+
74+
// Now it has three arguments
75+
const loadPlayer = async (context, options, controller) => {
76+
const res = await fetch(`/api/players/${context.playerId}`, { signal: controller.signal })
77+
if (!res.ok) throw new Error(res.statusText)
78+
return res.json()
79+
}
80+
81+
// With hooks
82+
const MyComponent = () => {
83+
const state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } })
84+
}
85+
86+
// With the Async component
87+
<Async promiseFn={loadPlayer} context={{ playerId: 1 }} />
88+
```
89+
90+
For the `deferFn` this means no longer expecting an array of arguments but instead a singular argument.
91+
The `run` now accepts only one argument which is a singular value. All other arguments to `run` but
92+
the first will be ignored.
93+
94+
So before in `< 10.0.0` you would do this:
95+
96+
```jsx
97+
import Async from "react-async"
98+
99+
const getAttendance = () =>
100+
fetch("/attendance").then(
101+
() => true,
102+
() => false
103+
)
104+
const updateAttendance = ([attend, userId]) =>
105+
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then(
106+
() => attend,
107+
() => !attend
108+
)
109+
110+
const userId = 42
111+
112+
const AttendanceToggle = () => (
113+
<Async promiseFn={getAttendance} deferFn={updateAttendance}>
114+
{({ isPending, data: isAttending, run, setData }) => (
115+
<Toggle
116+
on={isAttending}
117+
onClick={() => {
118+
run(!isAttending, userId)
119+
}}
120+
disabled={isPending}
121+
/>
122+
)}
123+
</Async>
124+
)
125+
```
126+
127+
In `11.0.0` you need to account for for the parameters not being an array:
128+
129+
```jsx
130+
import Async from "react-async"
131+
132+
const getAttendance = () =>
133+
fetch("/attendance").then(
134+
() => true,
135+
() => false
136+
)
137+
const updateAttendance = ({ attend, userId }) =>
138+
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then(
139+
() => attend,
140+
() => !attend
141+
)
142+
143+
const userId = 42
144+
145+
const AttendanceToggle = () => (
146+
<Async promiseFn={getAttendance} deferFn={updateAttendance}>
147+
{({ isPending, data: isAttending, run, setData }) => (
148+
<Toggle
149+
on={isAttending}
150+
onClick={() => {
151+
run({ attend: isAttending, userId })
152+
}}
153+
disabled={isPending}
154+
/>
155+
)}
156+
</Async>
157+
)
158+
```
159+
160+
Another thing you need to be careful about is the `watchFn` you can no longer count on the fact that
161+
unknown parameters are put into the `AsyncProps`. Before `< 10.0.0` you would write:
162+
163+
```ts
164+
useAsync({
165+
promiseFn,
166+
count: 0,
167+
watchFn: (props, prevProps) => props.count !== prevProps.count
168+
});
169+
```
170+
171+
In `11.0.0` you need to use the `context` instead:
172+
173+
```ts
174+
useAsync({
175+
promiseFn,
176+
context: { count: 0 },
177+
watchFn: (props, prevProps) => props.context.count !== prevProps.context.count
178+
});
179+
```
180+
181+
## Upgrade to v10
182+
183+
This is a major release due to the migration to TypeScript. While technically it shouldn't change anything, it might be a breaking change in certain situations. Theres also a bugfix for watchFn and a fix for legacy browsers.
184+
3185
## Upgrade to v9
4186
5187
The rejection value for failed requests with `useFetch` was changed. Previously it was the Response object. Now it's an

docs/getting-started/usage.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ The `useAsync` hook \(available [from React v16.8.0](https://reactjs.org/hooks)\
1010
import { useAsync } from "react-async"
1111

1212
// You can use async/await or any function that returns a Promise
13-
const loadPlayer = async ({ playerId }, { signal }) => {
13+
const loadPlayer = async ({ playerId }, options, { signal }) => {
1414
const res = await fetch(`/api/players/${playerId}`, { signal })
1515
if (!res.ok) throw new Error(res.statusText)
1616
return res.json()
1717
}
1818

1919
const MyComponent = () => {
20-
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
20+
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } })
2121
if (isPending) return "Loading..."
2222
if (error) return `Something went wrong: ${error.message}`
2323
if (data)
@@ -85,14 +85,14 @@ The classic interface to React Async. Simply use `<Async>` directly in your JSX
8585
import Async from "react-async"
8686

8787
// Your promiseFn receives all props from Async and an AbortController instance
88-
const loadPlayer = async ({ playerId }, { signal }) => {
88+
const loadPlayer = async ({ playerId }, options, { signal }) => {
8989
const res = await fetch(`/api/players/${playerId}`, { signal })
9090
if (!res.ok) throw new Error(res.statusText)
9191
return res.json()
9292
}
9393

9494
const MyComponent = () => (
95-
<Async promiseFn={loadPlayer} playerId={1}>
95+
<Async promiseFn={loadPlayer} context={{ playerId: 1}}>
9696
{({ data, error, isPending }) => {
9797
if (isPending) return "Loading..."
9898
if (error) return `Something went wrong: ${error.message}`
@@ -118,7 +118,7 @@ You can also create your own component instances, allowing you to preconfigure t
118118
```jsx
119119
import { createInstance } from "react-async"
120120

121-
const loadPlayer = async ({ playerId }, { signal }) => {
121+
const loadPlayer = async ({ playerId }, options, { signal }) => {
122122
const res = await fetch(`/api/players/${playerId}`, { signal })
123123
if (!res.ok) throw new Error(res.statusText)
124124
return res.json()
@@ -128,7 +128,7 @@ const loadPlayer = async ({ playerId }, { signal }) => {
128128
const AsyncPlayer = createInstance({ promiseFn: loadPlayer }, "AsyncPlayer")
129129

130130
const MyComponent = () => (
131-
<AsyncPlayer playerId={1}>
131+
<AsyncPlayer context={{playerId: 1}}>
132132
<AsyncPlayer.Fulfilled>{player => `Hello ${player.name}`}</AsyncPlayer.Fulfilled>
133133
</AsyncPlayer>
134134
)
@@ -141,12 +141,12 @@ Several [helper components](usage.md#helper-components) are available to improve
141141
```jsx
142142
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async"
143143

144-
const loadPlayer = async ({ playerId }, { signal }) => {
144+
const loadPlayer = async ({ playerId }, options, { signal }) => {
145145
// ...
146146
}
147147

148148
const MyComponent = () => {
149-
const state = useAsync({ promiseFn: loadPlayer, playerId: 1 })
149+
const state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } })
150150
return (
151151
<>
152152
<IfPending state={state}>Loading...</IfPending>
@@ -171,14 +171,14 @@ Each of the helper components are also available as static properties of `<Async
171171
```jsx
172172
import Async from "react-async"
173173

174-
const loadPlayer = async ({ playerId }, { signal }) => {
174+
const loadPlayer = async ({ playerId }, options, { signal }) => {
175175
const res = await fetch(`/api/players/${playerId}`, { signal })
176176
if (!res.ok) throw new Error(res.statusText)
177177
return res.json()
178178
}
179179

180180
const MyComponent = () => (
181-
<Async promiseFn={loadPlayer} playerId={1}>
181+
<Async promiseFn={loadPlayer} context={{playerId: 1 }}>
182182
<Async.Pending>Loading...</Async.Pending>
183183
<Async.Fulfilled>
184184
{data => (

docs/guide/async-actions.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ automatically invoked by React Async when rendering the component. Instead it wi
1212
import React, { useState } from "react"
1313
import { useAsync } from "react-async"
1414

15-
const subscribe = ([email], props, { signal }) =>
15+
const subscribe = ({email}, options, { signal }) =>
1616
fetch("/newsletter", { method: "POST", body: JSON.stringify({ email }), signal })
1717

1818
const NewsletterForm = () => {
@@ -21,7 +21,7 @@ const NewsletterForm = () => {
2121

2222
const handleSubmit = event => {
2323
event.preventDefault()
24-
run(email)
24+
run({email})
2525
}
2626

2727
return (
@@ -36,11 +36,11 @@ const NewsletterForm = () => {
3636
}
3737
```
3838

39-
As you can see, the `deferFn` is invoked with 3 arguments: `args`, `props` and the AbortController. `args` is an array
39+
As you can see, the `deferFn` is invoked with 3 arguments: `context`, `props` and the AbortController. `context` is an object
4040
representing the arguments that were passed to `run`. In this case we passed the `email`, so we can extract that from
41-
the `args` array at the first index using [array destructuring] and pass it along to our `fetch` request.
41+
the `context` prop using [object destructuring] and pass it along to our `fetch` request.
4242

43-
[array destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring
43+
[object destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring
4444

4545
## Sending data with `useFetch`
4646

docs/guide/async-components.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ The above example, written with `useAsync`, would look like this:
5252
import React from "react"
5353
import { useAsync } from "react-async"
5454

55-
const fetchPerson = async ({ id }, { signal }) => {
55+
const fetchPerson = async ({ id }, options, { signal }) => {
5656
const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal })
5757
if (!response.ok) throw new Error(response.status)
5858
return response.json()
5959
}
6060

6161
const Person = ({ id }) => {
62-
const { data, error } = useAsync({ promiseFn: fetchPerson, id })
62+
const { data, error } = useAsync({ promiseFn: fetchPerson, {context: id }})
6363
if (error) return error.message
6464
if (data) return `Hi, my name is ${data.name}!`
6565
return null
@@ -70,9 +70,9 @@ const App = () => {
7070
}
7171
```
7272

73-
Notice the incoming parameters to `fetchPerson`. The `promiseFn` will be invoked with a `props` object and an
74-
`AbortController`. `props` are the options you passed to `useAsync`, which is why you can access the `id` property
75-
using [object destructuring]. The `AbortController` is created by React Async to enable [abortable fetch], so the
73+
Notice the incoming parameters to `fetchPerson`. The `promiseFn` will be invoked with a `context`, `props` object and an
74+
`AbortController`. The `context` contains the `id` property which you can access
75+
using [object destructuring]. `props` are the options you passed to `useAsync`. The `AbortController` is created by React Async to enable [abortable fetch], so the
7676
underlying request will be aborted when the promise is cancelled (e.g. when a new one starts or we leave the page). We
7777
have to pass its `AbortSignal` down to `fetch` in order to wire this up.
7878

docs/guide/optimistic-updates.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const AttendanceToggle = () => (
3131
)
3232
```
3333

34-
Here we have a switch to toggle attentance for an event. Clicking the toggle will most likely succeed, so we can predict
34+
Here we have a switch to toggle attendance for an event. Clicking the toggle will most likely succeed, so we can predict
3535
the value it will have after completion (because we're just flipping a boolean).
3636

3737
Notice that React Async accepts both a `promiseFn` and a `deferFn` at the same time. This allows you to combine data

0 commit comments

Comments
 (0)