Skip to content

Commit

Permalink
improvement: deferFn and promiseFn now have the same signature.
Browse files Browse the repository at this point in the history
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: async-library#246
  • Loading branch information
MrHus committed Jan 21, 2020
1 parent 0d93635 commit ef374bf
Show file tree
Hide file tree
Showing 17 changed files with 404 additions and 93 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ lerna-debug.log*
# when working with contributors
package-lock.json
yarn.lock

.vscode
13 changes: 10 additions & 3 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,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.
## `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`.
Expand Down Expand Up @@ -132,3 +132,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`.
147 changes: 147 additions & 0 deletions docs/getting-started/upgrading.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,152 @@
# Upgrading

## Upgrade to v11

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`. For example:
```jsx
useAsync({ promiseFn: loadPlayer, playerId: 1 })
```
In the above example the context would be `{playerId: 1}`.
This means that you know need to expect three parameter for the `promiseFn` 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()
}

const MyComponent = () => {
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })
}
```
In `11.0.0` you need to account for the three parameters:
```jsx
import { useAsync } from "react-async"

// With two 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()
}

const MyComponent = () => {
// You can either pass arguments by adding extra keys to the AsyncProps
const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 })

// Or you can explicitly define the context which is TypeScript friendly
const { data, error, isPending } = useAsync({ 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>
)
```
## 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
Expand Down
14 changes: 7 additions & 7 deletions docs/getting-started/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -85,7 +85,7 @@ 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()
Expand Down Expand Up @@ -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()
Expand All @@ -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>
Expand All @@ -171,7 +171,7 @@ 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()
Expand Down
8 changes: 4 additions & 4 deletions examples/with-typescript/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import Async, {
IfPending,
IfRejected,
IfFulfilled,
PromiseFn,
AsyncFn,
} from "react-async"
import DevTools from "react-async-devtools"
import "./App.css"
import { FetchHookExample } from "./FetchHookExample"

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

const UseAsync = () => {
const state = useAsync({ promiseFn: loadFirstName, userId: 1 })
const state = useAsync({ promiseFn: loadFirstName, context: { userId: 1 } })
return (
<>
<IfPending state={state}>Loading...</IfPending>
Expand All @@ -47,7 +47,7 @@ class App extends Component {
<Async promiseFn={() => Promise.resolve("bar")}>
<Async.Resolved>{data => <>{data}</>}</Async.Resolved>
</Async>
<CustomAsync userId={1}>
<CustomAsync context={{ userId: 1 }}>
<CustomAsync.Resolved>{data => <>{data}</>}</CustomAsync.Resolved>
</CustomAsync>
<UseAsync />
Expand Down
2 changes: 1 addition & 1 deletion examples/with-typescript/src/FetchHookExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from "react"
import { useFetch } from "react-async"

export function FetchHookExample() {
const result = useFetch<{ token: string }>("https://reqres.in/api/login", {
const result = useFetch<{ token: string }, {}>("https://reqres.in/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
12 changes: 6 additions & 6 deletions packages/react-async/src/Async.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +284,20 @@ describe("createInstance", () => {
let counter = 1
const { getByText } = render(
<CustomAsync foo="bar">
{({ run }) => <button onClick={() => run("go", counter++)}>run</button>}
{({ run }) => <button onClick={() => run({ type: "go", counter: counter++ })}>run</button>}
</CustomAsync>
)
const expectedProps = { deferFn, foo: "bar" }
expect(deferFn).not.toHaveBeenCalled()
fireEvent.click(getByText("run"))
expect(deferFn).toHaveBeenCalledWith(
["go", 1],
{ type: "go", counter: 1 },
expect.objectContaining(expectedProps),
abortCtrl
)
fireEvent.click(getByText("run"))
expect(deferFn).toHaveBeenCalledWith(
["go", 2],
{ type: "go", counter: 2 },
expect.objectContaining(expectedProps),
abortCtrl
)
Expand All @@ -312,7 +312,7 @@ describe("createInstance", () => {
<CustomAsync foo="bar">
{({ run, reload }) =>
counter === 1 ? (
<button onClick={() => run("go", counter++)}>run</button>
<button onClick={() => run({ type: "go", counter: counter++ })}>run</button>
) : (
<button onClick={reload}>reload</button>
)
Expand All @@ -323,13 +323,13 @@ describe("createInstance", () => {
expect(deferFn).not.toHaveBeenCalled()
fireEvent.click(getByText("run"))
expect(deferFn).toHaveBeenCalledWith(
["go", 1],
{ type: "go", counter: 1 },
expect.objectContaining(expectedProps),
abortCtrl
)
fireEvent.click(getByText("reload"))
expect(deferFn).toHaveBeenCalledWith(
["go", 1],
{ type: "go", counter: 1 },
expect.objectContaining(expectedProps),
abortCtrl
)
Expand Down
Loading

0 comments on commit ef374bf

Please sign in to comment.