Skip to content

Commit ef374bf

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 all extra properties that are unknown to `AsyncProps` are put on the `AsyncProps`'s `context` property. This means that when using TypeScript you can now use `context` to safely extract the context. This does however mean that because `[prop: string]: any` is removed TypeScript users have a breaking change. Closes: #246
1 parent 0d93635 commit ef374bf

17 files changed

+404
-93
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

+10-3
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ A Promise instance which has already started. It will simply add the necessary r
3131
3232
## `promiseFn`
3333

34-
> `function(props: Object, controller: AbortController): Promise`
34+
> `function(context C, props: AsyncOptions, controller: AbortController): Promise`
3535
3636
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.
3737

3838
> 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.
3939
4040
## `deferFn`
4141

42-
> `function(args: any[], props: Object, controller: AbortController): Promise`
42+
> `function(context: C, props: AsyncOptions, controller: AbortController): Promise`
4343
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`.
44+
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`.
4545

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

docs/getting-started/upgrading.md

+147
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,152 @@
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`. For example:
29+
30+
```jsx
31+
useAsync({ promiseFn: loadPlayer, playerId: 1 })
32+
```
33+
34+
In the above example the context would be `{playerId: 1}`.
35+
36+
This means that you know need to expect three parameter for the `promiseFn` instead of two.
37+
38+
So before in `< 10.0.0` you would do this:
39+
40+
```jsx
41+
import { useAsync } from "react-async"
42+
43+
// Here loadPlayer has only two arguments
44+
const loadPlayer = async (options, controller) => {
45+
const res = await fetch(`/api/players/${options.playerId}`, { signal: controller.signal })
46+
if (!res.ok) throw new Error(res.statusText)
47+
return res.json()
48+
}
49+
50+
const MyComponent = () => {
51+
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
52+
}
53+
```
54+
55+
In `11.0.0` you need to account for the three parameters:
56+
57+
```jsx
58+
import { useAsync } from "react-async"
59+
60+
// With two arguments:
61+
const loadPlayer = async (context, options, controller) => {
62+
const res = await fetch(`/api/players/${context.playerId}`, { signal: controller.signal })
63+
if (!res.ok) throw new Error(res.statusText)
64+
return res.json()
65+
}
66+
67+
const MyComponent = () => {
68+
// You can either pass arguments by adding extra keys to the AsyncProps
69+
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
70+
71+
// Or you can explicitly define the context which is TypeScript friendly
72+
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } })
73+
}
74+
```
75+
76+
For the `deferFn` this means no longer expecting an array of arguments but instead a singular argument.
77+
The `run` now accepts only one argument which is a singular value. All other arguments to `run` but
78+
the first will be ignored.
79+
80+
So before in `< 10.0.0` you would do this:
81+
82+
```jsx
83+
import Async from "react-async"
84+
85+
const getAttendance = () =>
86+
fetch("/attendance").then(
87+
() => true,
88+
() => false
89+
)
90+
const updateAttendance = ([attend, userId]) =>
91+
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then(
92+
() => attend,
93+
() => !attend
94+
)
95+
96+
const userId = 42
97+
98+
const AttendanceToggle = () => (
99+
<Async promiseFn={getAttendance} deferFn={updateAttendance}>
100+
{({ isPending, data: isAttending, run, setData }) => (
101+
<Toggle
102+
on={isAttending}
103+
onClick={() => {
104+
run(!isAttending, userId)
105+
}}
106+
disabled={isPending}
107+
/>
108+
)}
109+
</Async>
110+
)
111+
```
112+
113+
In `11.0.0` you need to account for for the parameters not being an array:
114+
115+
```jsx
116+
import Async from "react-async"
117+
118+
const getAttendance = () =>
119+
fetch("/attendance").then(
120+
() => true,
121+
() => false
122+
)
123+
const updateAttendance = ({ attend, userId }) =>
124+
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then(
125+
() => attend,
126+
() => !attend
127+
)
128+
129+
const userId = 42
130+
131+
const AttendanceToggle = () => (
132+
<Async promiseFn={getAttendance} deferFn={updateAttendance}>
133+
{({ isPending, data: isAttending, run, setData }) => (
134+
<Toggle
135+
on={isAttending}
136+
onClick={() => {
137+
run({ attend: isAttending, userId })
138+
}}
139+
disabled={isPending}
140+
/>
141+
)}
142+
</Async>
143+
)
144+
```
145+
146+
## Upgrade to v10
147+
148+
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.
149+
3150
## Upgrade to v9
4151
5152
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

+7-7
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,7 +85,7 @@ 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()
@@ -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()
@@ -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,7 +171,7 @@ 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()

examples/with-typescript/src/App.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import Async, {
55
IfPending,
66
IfRejected,
77
IfFulfilled,
8-
PromiseFn,
8+
AsyncFn,
99
} from "react-async"
1010
import DevTools from "react-async-devtools"
1111
import "./App.css"
1212
import { FetchHookExample } from "./FetchHookExample"
1313

14-
const loadFirstName: PromiseFn<string> = ({ userId }) =>
14+
const loadFirstName: AsyncFn<string, { userId: number }> = ({ userId }) =>
1515
fetch(`https://reqres.in/api/users/${userId}`)
1616
.then(res => (res.ok ? Promise.resolve(res) : Promise.reject(res)))
1717
.then(res => res.json())
@@ -20,7 +20,7 @@ const loadFirstName: PromiseFn<string> = ({ userId }) =>
2020
const CustomAsync = createInstance({ promiseFn: loadFirstName })
2121

2222
const UseAsync = () => {
23-
const state = useAsync({ promiseFn: loadFirstName, userId: 1 })
23+
const state = useAsync({ promiseFn: loadFirstName, context: { userId: 1 } })
2424
return (
2525
<>
2626
<IfPending state={state}>Loading...</IfPending>
@@ -47,7 +47,7 @@ class App extends Component {
4747
<Async promiseFn={() => Promise.resolve("bar")}>
4848
<Async.Resolved>{data => <>{data}</>}</Async.Resolved>
4949
</Async>
50-
<CustomAsync userId={1}>
50+
<CustomAsync context={{ userId: 1 }}>
5151
<CustomAsync.Resolved>{data => <>{data}</>}</CustomAsync.Resolved>
5252
</CustomAsync>
5353
<UseAsync />

examples/with-typescript/src/FetchHookExample.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from "react"
22
import { useFetch } from "react-async"
33

44
export function FetchHookExample() {
5-
const result = useFetch<{ token: string }>("https://reqres.in/api/login", {
5+
const result = useFetch<{ token: string }, {}>("https://reqres.in/api/login", {
66
method: "POST",
77
headers: {
88
"Content-Type": "application/json",

packages/react-async/src/Async.spec.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -284,20 +284,20 @@ describe("createInstance", () => {
284284
let counter = 1
285285
const { getByText } = render(
286286
<CustomAsync foo="bar">
287-
{({ run }) => <button onClick={() => run("go", counter++)}>run</button>}
287+
{({ run }) => <button onClick={() => run({ type: "go", counter: counter++ })}>run</button>}
288288
</CustomAsync>
289289
)
290290
const expectedProps = { deferFn, foo: "bar" }
291291
expect(deferFn).not.toHaveBeenCalled()
292292
fireEvent.click(getByText("run"))
293293
expect(deferFn).toHaveBeenCalledWith(
294-
["go", 1],
294+
{ type: "go", counter: 1 },
295295
expect.objectContaining(expectedProps),
296296
abortCtrl
297297
)
298298
fireEvent.click(getByText("run"))
299299
expect(deferFn).toHaveBeenCalledWith(
300-
["go", 2],
300+
{ type: "go", counter: 2 },
301301
expect.objectContaining(expectedProps),
302302
abortCtrl
303303
)
@@ -312,7 +312,7 @@ describe("createInstance", () => {
312312
<CustomAsync foo="bar">
313313
{({ run, reload }) =>
314314
counter === 1 ? (
315-
<button onClick={() => run("go", counter++)}>run</button>
315+
<button onClick={() => run({ type: "go", counter: counter++ })}>run</button>
316316
) : (
317317
<button onClick={reload}>reload</button>
318318
)
@@ -323,13 +323,13 @@ describe("createInstance", () => {
323323
expect(deferFn).not.toHaveBeenCalled()
324324
fireEvent.click(getByText("run"))
325325
expect(deferFn).toHaveBeenCalledWith(
326-
["go", 1],
326+
{ type: "go", counter: 1 },
327327
expect.objectContaining(expectedProps),
328328
abortCtrl
329329
)
330330
fireEvent.click(getByText("reload"))
331331
expect(deferFn).toHaveBeenCalledWith(
332-
["go", 1],
332+
{ type: "go", counter: 1 },
333333
expect.objectContaining(expectedProps),
334334
abortCtrl
335335
)

0 commit comments

Comments
 (0)