-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvement/add context to async options #247
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,5 @@ lerna-debug.log* | |
# when working with contributors | ||
package-lock.json | ||
yarn.lock | ||
|
||
.vscode | ||
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ These can be passed in an object to `useAsync(options)`, or as props to `<Async | |||||
- [`promise`](#promise) An already started Promise instance. | ||||||
- [`promiseFn`](#promisefn) Function that returns a Promise, automatically invoked. | ||||||
- [`deferFn`](#deferfn) Function that returns a Promise, manually invoked with `run`. | ||||||
- [`context`](#context) The first argument for the `promise` and `promiseFn` function. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
- [`watch`](#watch) Watch a value and automatically reload when it changes. | ||||||
- [`watchFn`](#watchfn) Watch this function and automatically reload when it returns truthy. | ||||||
- [`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 | |||||
## `promiseFn` | ||||||
|
||||||
> `function(props: Object, controller: AbortController): Promise` | ||||||
> `function(context C, props: AsyncOptions, controller: AbortController): Promise` | ||||||
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. | ||||||
|
||||||
> 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. | ||||||
> 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
## `deferFn` | ||||||
|
||||||
> `function(args: any[], props: Object, controller: AbortController): Promise` | ||||||
> `function(context: C, props: AsyncOptions, controller: AbortController): Promise` | ||||||
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`. | ||||||
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`. | ||||||
|
||||||
> Be aware that when using both `promiseFn` and `deferFn`, the shape of their fulfilled value should match, because they both update the same `data`. | ||||||
|
@@ -132,3 +133,10 @@ Enables the use of `deferFn` if `true`, or enables the use of `promiseFn` if `fa | |||||
> `boolean` | ||||||
Enables or disables JSON parsing of the response body. By default this is automatically enabled if the `Accept` header is set to `"application/json"`. | ||||||
|
||||||
|
||||||
## `context` | ||||||
|
||||||
> `C | undefined` | ||||||
The argument which is passed as the first argument to the `promiseFn` and the `deferFn`. |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,5 +1,206 @@ | ||||||||||||||||
# Upgrading | ||||||||||||||||
|
||||||||||||||||
## Upgrade to v11 | ||||||||||||||||
|
||||||||||||||||
### promiseFn and deferFn unification | ||||||||||||||||
|
||||||||||||||||
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 difference is the idea of having a `context`, the context will contain all parameters | ||||||||||||||||
to `AsyncProps` which are not native to the `AsyncProps`. Before you could pass any parameter | ||||||||||||||||
to `AsyncProps` and it would pass them to the `deferFn` and `promiseFn`, now you need to use | ||||||||||||||||
the `context` instead. | ||||||||||||||||
Comment on lines
+29
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||
|
||||||||||||||||
For example before you could write: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
useAsync({ promiseFn: loadPlayer, playerId: 1 }) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
Now you must write: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
useAsync({ promiseFn: loadPlayer, context: { playerId: 1 }}) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In the above example the context would be `{playerId: 1}`. | ||||||||||||||||
|
||||||||||||||||
This means that `promiseFn` now expects three parameters instead of two. | ||||||||||||||||
|
||||||||||||||||
So before in `< 10.0.0` you would do this: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
import { useAsync } from "react-async" | ||||||||||||||||
|
||||||||||||||||
// Here loadPlayer has only two arguments | ||||||||||||||||
const loadPlayer = async (options, controller) => { | ||||||||||||||||
const res = await fetch(`/api/players/${options.playerId}`, { signal: controller.signal }) | ||||||||||||||||
if (!res.ok) throw new Error(res.statusText) | ||||||||||||||||
return res.json() | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// With hooks | ||||||||||||||||
const MyComponent = () => { | ||||||||||||||||
const state = useAsync({ promiseFn: loadPlayer, playerId: 1 }) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// With the Async component | ||||||||||||||||
<Async promiseFn={loadPlayer} playerId={1} /> | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In `11.0.0` you need to account for the three parameters: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
import { useAsync } from "react-async" | ||||||||||||||||
|
||||||||||||||||
// Now it has three arguments | ||||||||||||||||
const loadPlayer = async (context, options, controller) => { | ||||||||||||||||
const res = await fetch(`/api/players/${context.playerId}`, { signal: controller.signal }) | ||||||||||||||||
if (!res.ok) throw new Error(res.statusText) | ||||||||||||||||
return res.json() | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// With hooks | ||||||||||||||||
const MyComponent = () => { | ||||||||||||||||
const state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } }) | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// With the Async component | ||||||||||||||||
<Async promiseFn={loadPlayer} context={{ playerId: 1 }} /> | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
For the `deferFn` this means no longer expecting an array of arguments but instead a singular argument. | ||||||||||||||||
The `run` now accepts only one argument which is a singular value. All other arguments to `run` but | ||||||||||||||||
the first will be ignored. | ||||||||||||||||
|
||||||||||||||||
So before in `< 10.0.0` you would do this: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
import Async from "react-async" | ||||||||||||||||
|
||||||||||||||||
const getAttendance = () => | ||||||||||||||||
fetch("/attendance").then( | ||||||||||||||||
() => true, | ||||||||||||||||
() => false | ||||||||||||||||
) | ||||||||||||||||
const updateAttendance = ([attend, userId]) => | ||||||||||||||||
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then( | ||||||||||||||||
() => attend, | ||||||||||||||||
() => !attend | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
const userId = 42 | ||||||||||||||||
|
||||||||||||||||
const AttendanceToggle = () => ( | ||||||||||||||||
<Async promiseFn={getAttendance} deferFn={updateAttendance}> | ||||||||||||||||
{({ isPending, data: isAttending, run, setData }) => ( | ||||||||||||||||
<Toggle | ||||||||||||||||
on={isAttending} | ||||||||||||||||
onClick={() => { | ||||||||||||||||
run(!isAttending, userId) | ||||||||||||||||
}} | ||||||||||||||||
disabled={isPending} | ||||||||||||||||
/> | ||||||||||||||||
)} | ||||||||||||||||
</Async> | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In `11.0.0` you need to account for for the parameters not being an array: | ||||||||||||||||
|
||||||||||||||||
```jsx | ||||||||||||||||
import Async from "react-async" | ||||||||||||||||
|
||||||||||||||||
const getAttendance = () => | ||||||||||||||||
fetch("/attendance").then( | ||||||||||||||||
() => true, | ||||||||||||||||
() => false | ||||||||||||||||
) | ||||||||||||||||
const updateAttendance = ({ attend, userId }) => | ||||||||||||||||
fetch(`/attendance/${userId}`, { method: attend ? "POST" : "DELETE" }).then( | ||||||||||||||||
() => attend, | ||||||||||||||||
() => !attend | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
const userId = 42 | ||||||||||||||||
|
||||||||||||||||
const AttendanceToggle = () => ( | ||||||||||||||||
<Async promiseFn={getAttendance} deferFn={updateAttendance}> | ||||||||||||||||
{({ isPending, data: isAttending, run, setData }) => ( | ||||||||||||||||
<Toggle | ||||||||||||||||
on={isAttending} | ||||||||||||||||
onClick={() => { | ||||||||||||||||
run({ attend: isAttending, userId }) | ||||||||||||||||
}} | ||||||||||||||||
disabled={isPending} | ||||||||||||||||
/> | ||||||||||||||||
)} | ||||||||||||||||
</Async> | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### useAsync only accepts one prop | ||||||||||||||||
|
||||||||||||||||
Before in `10.0.0` you could call useAsync with multiple parameters, | ||||||||||||||||
the first argument would then be the `promiseFn` like this: | ||||||||||||||||
|
||||||||||||||||
```tsx | ||||||||||||||||
const state = useAsync(loadPlayer, { context: { playerId: 1 } }) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In `11.0.0` there is only one parameter. So the overload no longer works and you need to write this instead: | ||||||||||||||||
|
||||||||||||||||
```tsx | ||||||||||||||||
const state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } }) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### WatchFn | ||||||||||||||||
|
||||||||||||||||
Another thing you need to be careful about is the `watchFn` you can no longer count on the fact that | ||||||||||||||||
unknown parameters are put into the `AsyncProps`. Before `< 10.0.0` you would write: | ||||||||||||||||
|
||||||||||||||||
```ts | ||||||||||||||||
useAsync({ | ||||||||||||||||
promiseFn, | ||||||||||||||||
count: 0, | ||||||||||||||||
watchFn: (props, prevProps) => props.count !== prevProps.count | ||||||||||||||||
}); | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
In `11.0.0` you need to use the `context` instead: | ||||||||||||||||
|
||||||||||||||||
```ts | ||||||||||||||||
useAsync({ | ||||||||||||||||
promiseFn, | ||||||||||||||||
context: { count: 0 }, | ||||||||||||||||
watchFn: (props, prevProps) => props.context.count !== prevProps.context.count | ||||||||||||||||
}); | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
## Upgrade to v10 | ||||||||||||||||
|
||||||||||||||||
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. | ||||||||||||||||
|
||||||||||||||||
## Upgrade to v9 | ||||||||||||||||
|
||||||||||||||||
The rejection value for failed requests with `useFetch` was changed. Previously it was the Response object. Now it's an | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,14 +10,14 @@ The `useAsync` hook \(available [from React v16.8.0](https://reactjs.org/hooks)\ | |
import { useAsync } from "react-async" | ||
|
||
// You can use async/await or any function that returns a Promise | ||
const loadPlayer = async ({ playerId }, { signal }) => { | ||
const loadPlayer = async ({ playerId }, options, { signal }) => { | ||
const res = await fetch(`/api/players/${playerId}`, { signal }) | ||
if (!res.ok) throw new Error(res.statusText) | ||
return res.json() | ||
} | ||
|
||
const MyComponent = () => { | ||
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 }) | ||
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } }) | ||
if (isPending) return "Loading..." | ||
if (error) return `Something went wrong: ${error.message}` | ||
if (data) | ||
|
@@ -37,7 +37,7 @@ Or using the shorthand version: | |
|
||
```jsx | ||
const MyComponent = () => { | ||
const { data, error, isPending } = useAsync(loadPlayer, options) | ||
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } }) | ||
// ... | ||
} | ||
``` | ||
Comment on lines
38
to
43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example no longer makes any sense if you're going to just remove the shorthand version, so this entire section should be removed. |
||
|
@@ -85,14 +85,14 @@ The classic interface to React Async. Simply use `<Async>` directly in your JSX | |
import Async from "react-async" | ||
|
||
// Your promiseFn receives all props from Async and an AbortController instance | ||
const loadPlayer = async ({ playerId }, { signal }) => { | ||
const loadPlayer = async ({ playerId }, options, { signal }) => { | ||
const res = await fetch(`/api/players/${playerId}`, { signal }) | ||
if (!res.ok) throw new Error(res.statusText) | ||
return res.json() | ||
} | ||
|
||
const MyComponent = () => ( | ||
<Async promiseFn={loadPlayer} playerId={1}> | ||
<Async promiseFn={loadPlayer} context={{ playerId: 1}}> | ||
{({ data, error, isPending }) => { | ||
if (isPending) return "Loading..." | ||
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 | |
```jsx | ||
import { createInstance } from "react-async" | ||
|
||
const loadPlayer = async ({ playerId }, { signal }) => { | ||
const loadPlayer = async ({ playerId }, options, { signal }) => { | ||
const res = await fetch(`/api/players/${playerId}`, { signal }) | ||
if (!res.ok) throw new Error(res.statusText) | ||
return res.json() | ||
|
@@ -128,7 +128,7 @@ const loadPlayer = async ({ playerId }, { signal }) => { | |
const AsyncPlayer = createInstance({ promiseFn: loadPlayer }, "AsyncPlayer") | ||
|
||
const MyComponent = () => ( | ||
<AsyncPlayer playerId={1}> | ||
<AsyncPlayer context={{playerId: 1}}> | ||
<AsyncPlayer.Fulfilled>{player => `Hello ${player.name}`}</AsyncPlayer.Fulfilled> | ||
</AsyncPlayer> | ||
) | ||
|
@@ -141,12 +141,12 @@ Several [helper components](usage.md#helper-components) are available to improve | |
```jsx | ||
import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async" | ||
|
||
const loadPlayer = async ({ playerId }, { signal }) => { | ||
const loadPlayer = async ({ playerId }, options, { signal }) => { | ||
// ... | ||
} | ||
|
||
const MyComponent = () => { | ||
const state = useAsync({ promiseFn: loadPlayer, playerId: 1 }) | ||
const state = useAsync({ promiseFn: loadPlayer, context: { playerId: 1 } }) | ||
return ( | ||
<> | ||
<IfPending state={state}>Loading...</IfPending> | ||
|
@@ -171,14 +171,14 @@ Each of the helper components are also available as static properties of `<Async | |
```jsx | ||
import Async from "react-async" | ||
|
||
const loadPlayer = async ({ playerId }, { signal }) => { | ||
const loadPlayer = async ({ playerId }, options, { signal }) => { | ||
const res = await fetch(`/api/players/${playerId}`, { signal }) | ||
if (!res.ok) throw new Error(res.statusText) | ||
return res.json() | ||
} | ||
|
||
const MyComponent = () => ( | ||
<Async promiseFn={loadPlayer} playerId={1}> | ||
<Async promiseFn={loadPlayer} context={{playerId: 1 }}> | ||
<Async.Pending>Loading...</Async.Pending> | ||
<Async.Fulfilled> | ||
{data => ( | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -12,7 +12,7 @@ automatically invoked by React Async when rendering the component. Instead it wi | |||||
import React, { useState } from "react" | ||||||
import { useAsync } from "react-async" | ||||||
|
||||||
const subscribe = ([email], props, { signal }) => | ||||||
const subscribe = ({email}, options, { signal }) => | ||||||
fetch("/newsletter", { method: "POST", body: JSON.stringify({ email }), signal }) | ||||||
|
||||||
const NewsletterForm = () => { | ||||||
|
@@ -21,7 +21,7 @@ const NewsletterForm = () => { | |||||
|
||||||
const handleSubmit = event => { | ||||||
event.preventDefault() | ||||||
run(email) | ||||||
run({email}) | ||||||
} | ||||||
|
||||||
return ( | ||||||
|
@@ -36,11 +36,11 @@ const NewsletterForm = () => { | |||||
} | ||||||
``` | ||||||
|
||||||
As you can see, the `deferFn` is invoked with 3 arguments: `args`, `props` and the AbortController. `args` is an array | ||||||
As you can see, the `deferFn` is invoked with 3 arguments: `context`, `props` and the AbortController. `context` is an object | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oxford comma.
Suggested change
|
||||||
representing the arguments that were passed to `run`. In this case we passed the `email`, so we can extract that from | ||||||
the `args` array at the first index using [array destructuring] and pass it along to our `fetch` request. | ||||||
the `context` prop using [object destructuring] and pass it along to our `fetch` request. | ||||||
|
||||||
[array destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring | ||||||
[object destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring | ||||||
|
||||||
## Sending data with `useFetch` | ||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be in your global gitignore too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added it because the project explicitly instructs you to create settings for
.vscode
in theCONTRIBUTING.md
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree we should add it. Just wanted to let you know you can use a global gitignore file for it too.