Skip to content

Commit 5286c08

Browse files
committed
Add initial getAsyncThunk API docs and usage guide
1 parent 1076ab2 commit 5286c08

File tree

3 files changed

+382
-0
lines changed

3 files changed

+382
-0
lines changed

docs/api/createAsyncThunk.md

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
---
2+
id: createAsyncThunk
3+
title: createAsyncThunk
4+
sidebar_label: createAsyncThunk
5+
hide_title: true
6+
---
7+
8+
# `createAsyncThunk`
9+
10+
## Overview
11+
12+
A function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the provided action type, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
13+
14+
This abstracts the standard recommended approach for handling async request lifecycles.
15+
16+
Sample usage:
17+
18+
```js {5-11,22-25,30}
19+
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
20+
import { userAPI } from './userAPI'
21+
22+
// First, create the thunk
23+
const fetchUserById = createAsyncThunk(
24+
'users/fetchByIdStatus',
25+
async (userId, thunkAPI) => {
26+
const response = await userAPI.fetchById(userId)
27+
return response.data
28+
}
29+
)
30+
31+
// Then, handle actions in your reducers:
32+
const usersSlice = createSlice({
33+
name: 'users',
34+
initialState: { entities: [], loading: 'idle' },
35+
reducers: {
36+
// standard reducer logic, with auto-generated action types per reducer
37+
},
38+
extraReducers: {
39+
// Add reducers for additional action types here, and handle loading state as needed
40+
[fetchUserById.fulfilled]: (state, action) => {
41+
// Add user to the state array
42+
state.entities.push(action.payload)
43+
}
44+
}
45+
})
46+
47+
// Later, dispatch the thunk as needed in the app
48+
dispatch(fetchUserById(123))
49+
```
50+
51+
## Parameters
52+
53+
### `type`
54+
55+
A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request:
56+
57+
For example, a `type` argument of `'users/requestStatus'` will generate these action types:
58+
59+
- `pending`: `'users/requestStatus/pending'`
60+
- `fulfilled`: `'users/requestStatus/fulfilled'`
61+
- `rejected`: `'users/requestStatus/rejected'`
62+
63+
### `payloadCreator`
64+
65+
A callback function that should return a promise containing the result of some asynchronous logic. It may also return a value synchronously. If there is an error, it should return a rejected promise containing either an `Error` instance or a plain value such as a descriptive error message.
66+
67+
The `payloadCreator` function can contain whatever logic you need to calculate an appropriate result. This could include a standard AJAX data fetch request, multiple AJAX calls with the results combined into a final value, interactions with React Native `AsyncStorage`, and so on.
68+
69+
The `payloadCreator` function will be called with two arguments:
70+
71+
- `thunkArg`: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. This is useful for passing in values like item IDs that may be needed as part of the request. If you need to pass in multiple values, pass them together in an object when you dispatch the thunk, like `dispatch(fetchUsers({status: 'active', sortBy: 'name'}))`.
72+
- `thunkAPI`: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options:
73+
- `dispatch`: the Redux store `dispatch` method
74+
- `getState`: the Redux store `getState` method
75+
- `extra`: the "extra argument" given to the thunk middleware on setup, if available
76+
- `requestId`: a unique string ID value that was automatically generated to identify this request sequence
77+
- `signal`: an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal) that may be used to see if another part of the app logic has marked this request as needing cancelation.
78+
79+
The logic in the `payloadCreator` function may use any of these values as needed to calculate the result.
80+
81+
## Return Value
82+
83+
`createAsyncThunk` returns a standard Redux thunk action creator. The thunk action creator function will have plain action creators for the `pending`, `fulfilled`, and `rejected` cases attached as nested fields.
84+
85+
When dispatched, the thunk will:
86+
87+
- dispatch the `pending` action
88+
- call the `payloadCreator` callback and wait for the returned promise to settle
89+
- when the promise settles:
90+
- if the promise resolved successfully, dispatch the `fulfilled` action with the promise value as `action.payload`
91+
- if the promise failed, dispatch the `rejected` action with a serialized version of the error value as `action.error`
92+
- Return a fulfilled promise containing the final dispatched action (either the `fulfilled` or `rejected` action object)
93+
94+
## Promise Lifecycle Actions
95+
96+
`createAsyncThunk` will generate three Redux action creators using [`createAction`](./createAction.md): `pending`, `fulfilled`, and `rejected`. Each lifecycle action creator will be attached to the returned thunk action creator so that your reducer logic can reference the action types and respond to the actions when dispatched. Each action object will contain the current unique `requestId` and `args` values under `action.meta`.
97+
98+
The action creators will have these signatures:
99+
100+
```ts
101+
interface SerializedError {
102+
name?: string
103+
message?: string
104+
code?: string
105+
stack?: string
106+
}
107+
108+
interface PendingAction<ThunkArg> {
109+
type: string
110+
payload: undefined
111+
meta: {
112+
requestId: string
113+
thunkArg: ThunkArg
114+
}
115+
}
116+
117+
interface FulfilledAction<ThunkArg, PromiseResult> {
118+
type: string
119+
payload: PromiseResult
120+
meta: {
121+
requestId: string
122+
thunkArg: ThunkArg
123+
}
124+
}
125+
126+
interface RejectedAction<ThunkArg> {
127+
type: string
128+
payload: undefined
129+
error: SerializedError | any
130+
meta: {
131+
requestId: string
132+
thunkArg: ThunkArg
133+
aborted: boolean
134+
abortReason?: string
135+
}
136+
}
137+
138+
type Pending = <ThunkArg>(
139+
requestId: string,
140+
thunkArg: ThunkArg
141+
) => PendingAction<ThunkArg>
142+
143+
type Fulfilled = <ThunkArg, PromiseResult>(
144+
payload: PromiseResult,
145+
requestId: string,
146+
thunkArg: ThunkArg
147+
) => FulfilledAction<ThunkArg, PromiseResult>
148+
149+
type Rejected = <ThunkArg>(
150+
requestId: string,
151+
thunkArg: ThunkArg
152+
) => RejectedAction<ThunkArg>
153+
```
154+
155+
To handle these actions in your reducers, reference the action creators in `createReducer` or `createSlice` using either the object key notation or the "builder callback" notation:
156+
157+
```js {2,6,14,23}
158+
const reducer1 = createReducer(initialState, {
159+
[fetchUserById.fulfilled]: (state, action) => {}
160+
})
161+
162+
const reducer2 = createReducer(initialState, build => {
163+
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
164+
})
165+
166+
const reducer3 = createSlice({
167+
name: 'users',
168+
initialState,
169+
reducers: {},
170+
extraReducers: {
171+
[fetchUserById.fulfilled]: (state, action) => {}
172+
}
173+
})
174+
175+
const reducer4 = createSlice({
176+
name: 'users',
177+
initialState,
178+
reducers: {},
179+
extraReducers: builder => {
180+
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
181+
}
182+
})
183+
```
184+
185+
## Handling Thunk Results
186+
187+
Thunks may return a value when dispatched. A common use case is to return a promise from the thunk, dispatch the thunk from a component, and then wait for the promise to resolve before doing additional work:
188+
189+
```js
190+
const onClick = () => {
191+
dispatch(fetchUserById(userId)).then(() => {
192+
// do additional work
193+
})
194+
}
195+
```
196+
197+
The thunks generated by `createAsyncThunk` will always return a resolved promise with either the `fulfilled` action object or `rejected` action object inside, as appropriate.
198+
199+
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an `unwrapResult` function that can be used to extract the `payload` or `error` from the action and return or throw the result:
200+
201+
```js
202+
import { unwrapResult } from '@reduxjs/toolkit'
203+
204+
// in the component
205+
const onClick = () => {
206+
dispatch(fetchUserById(userId))
207+
.then(unwrapResult)
208+
.then(originalPromiseResult => {})
209+
.catch(serializedError => {})
210+
}
211+
```
212+
213+
## Examples
214+
215+
Requesting a user by ID, with loading state, and only one request at a time:
216+
217+
```js
218+
import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
219+
import { userAPI } from './userAPI'
220+
221+
const fetchUserById = createAsyncThunk(
222+
'users/fetchByIdStatus',
223+
async (userId, { getState }) => {
224+
const { loading } = getState().users
225+
if (loading !== 'idle') {
226+
return
227+
}
228+
const response = await userAPI.fetchById(userId)
229+
return response.data
230+
}
231+
)
232+
233+
const usersSlice = createSlice({
234+
name: 'users',
235+
initialState: {
236+
entities: [],
237+
loading: 'idle',
238+
error: null
239+
},
240+
reducers: {},
241+
extraReducers: {
242+
[fetchUserById.pending]: (state, action) => {
243+
if (state.loading === 'idle') {
244+
state.loading = 'pending'
245+
}
246+
},
247+
[fetchUserById.fulfilled]: (state, action) => {
248+
if (state.loading === 'pending') {
249+
state.loading = 'idle'
250+
state.push(action.payload)
251+
}
252+
},
253+
[fetchUserById.rejected]: (state, action) => {
254+
if (state.loading === 'pending') {
255+
state.loading = 'idle'
256+
state.error = action.error
257+
}
258+
}
259+
}
260+
})
261+
262+
const UsersComponent = () => {
263+
const { users, loading, error } = useSelector(state => state.users)
264+
const dispatch = useDispatch()
265+
266+
const fetchOneUser = async userId => {
267+
try {
268+
const resultAction = dispatch(fetchUserById(userId))
269+
const user = unwrapResult(resultAction)
270+
showToast('success', `Fetched ${user.name}`)
271+
} catch (err) {
272+
showToast('error', `Fetch failed: ${err.message}`)
273+
}
274+
}
275+
276+
// render UI here
277+
}
278+
```

docs/usage/usage-guide.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,3 +526,106 @@ This CodeSandbox example demonstrates the problem:
526526
<iframe src="https://codesandbox.io/embed/rw7ppj4z0m" style={{ width: '100%', height: '500px', border: 0, borderRadius: '4px', overflow: 'hidden' }} sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
527527

528528
If you encounter this, you may need to restructure your code in a way that avoids the circular references.
529+
530+
## Managing Async Requests
531+
532+
### Redux Data Fetching Patterns
533+
534+
Data fetching logic for Redux typically follows a predictable pattern:
535+
536+
- A "start" action is dispatched before the request, to indicate that the request is in progress. This may be used to track loading state to allow skipping duplicate requests or show loading indicators in the UI.
537+
- The async request is made
538+
- Depending on the request result, the async logic dispatches either a "success" action containing the result data, or a "failure" action containing error details. The reducer logic clears the loading state in both cases, and either processes the result data from the success case, or stores the error value for potential display.
539+
540+
These steps are not required, but are [recommended in the Redux tutorials as a suggested pattern](https://redux.js.org/advanced/async-actions).
541+
542+
A typical implementation might look like:
543+
544+
```js
545+
const getRepoDetailsStarted = () => ({
546+
type: "repoDetails/fetchStarted"
547+
})
548+
549+
const getRepoDetailsSuccess = (repoDetails) => {
550+
type: "repoDetails/fetchSucceeded",
551+
payload: repoDetails
552+
}
553+
554+
const getRepoDetailsFailed = (error) => {
555+
type: "repoDetails/fetchFailed",
556+
error
557+
}
558+
559+
const fetchIssuesCount = (org, repo) => async dispatch => {
560+
dispatch(getRepoDetailsStarted())
561+
try {
562+
const repoDetails = await getRepoDetails(org, repo)
563+
dispatch(getRepoDetailsSuccess(repoDetails))
564+
} catch (err) {
565+
dispatch(getRepoDetailsFailed(err.toString()))
566+
}
567+
}
568+
```
569+
570+
However, writing code using this approach is tedious. Each separate type of request needs repeated similar implementation:
571+
572+
- Unique action types need to be defined for the three different cases
573+
- Each of those action types usually has a corresponding action creator function
574+
- A thunk has to be written that dispatches the correct actions in the right sequence
575+
576+
`createAsyncThunk` abstracts this pattern by generating the action types and action creators and generating a thunk that dispatches those actions.
577+
578+
### Async Requests with `createAsyncThunk`
579+
580+
As a developer, you are probably most concerned with the actual logic needed to make an API request, what action type names show up in the Redux action history log, and how your reducers should process the fetched data. The repetitive details of defining the multiple action types and dispatching the actions in the right sequence aren't what matters.
581+
582+
`createAsyncThunk` simplifies this process - you only need to provide a string for the action type prefix and a payload creator callback that does the actual async logic and returns a promise with the result. In return, `createAsyncThunk` will give you a thunk that will take care of dispatching the right actions based on the promise you return, and action types that you can handle in your reducers:
583+
584+
```js {5-11,22-25,30}
585+
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
586+
import { userAPI } from './userAPI'
587+
588+
// First, create the thunk
589+
const fetchUserById = createAsyncThunk(
590+
'users/fetchByIdStatus',
591+
async (userId, thunkAPI) => {
592+
const response = await userAPI.fetchById(userId)
593+
return response.data
594+
}
595+
)
596+
597+
// Then, handle actions in your reducers:
598+
const usersSlice = createSlice({
599+
name: 'users',
600+
initialState: { entities: [], loading: 'idle' },
601+
reducers: {
602+
// standard reducer logic, with auto-generated action types per reducer
603+
},
604+
extraReducers: {
605+
// Add reducers for additional action types here, and handle loading state as needed
606+
[fetchUserById.fulfilled]: (state, action) => {
607+
// Add user to the state array
608+
state.entities.push(action.payload)
609+
}
610+
}
611+
})
612+
613+
// Later, dispatch the thunk as needed in the app
614+
dispatch(fetchUserById(123))
615+
```
616+
617+
The thunk action creator accepts a single argument, which will be passed as the first argument to your payload creator callback.
618+
619+
The payload creator will also receive a `thunkAPI` object containing the parameters that are normally passed to a standard Redux thunk function, as well as an auto-generated unique random request ID string and an [`AbortController.signal` object](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal):
620+
621+
```ts
622+
interface ThunkAPI {
623+
dispatch: Function
624+
getState: Function
625+
extra?: any
626+
requestId: string
627+
signal: AbortSignal
628+
}
629+
```
630+
631+
You can use any of these as needed inside the payload callback to determine what the final result should be.

website/sidebars.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"api/createAction",
1515
"api/createSlice",
1616
"api/createSelector",
17+
"api/createAsyncThunk",
1718
"api/other-exports"
1819
]
1920
}

0 commit comments

Comments
 (0)