|
| 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 | +`createAsyncThunk` accepts two parameters: a string action `type` value, and a `payloadCreator` callback. |
| 54 | + |
| 55 | +### `type` |
| 56 | + |
| 57 | +A string that will be used to generate additional Redux action type constants, representing the lifecycle of an async request: |
| 58 | + |
| 59 | +For example, a `type` argument of `'users/requestStatus'` will generate these action types: |
| 60 | + |
| 61 | +- `pending`: `'users/requestStatus/pending'` |
| 62 | +- `fulfilled`: `'users/requestStatus/fulfilled'` |
| 63 | +- `rejected`: `'users/requestStatus/rejected'` |
| 64 | + |
| 65 | +### `payloadCreator` |
| 66 | + |
| 67 | +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. |
| 68 | + |
| 69 | +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. |
| 70 | + |
| 71 | +The `payloadCreator` function will be called with two arguments: |
| 72 | + |
| 73 | +- `arg`: 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'}))`. |
| 74 | +- `thunkAPI`: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: |
| 75 | + - `dispatch`: the Redux store `dispatch` method |
| 76 | + - `getState`: the Redux store `getState` method |
| 77 | + - `extra`: the "extra argument" given to the thunk middleware on setup, if available |
| 78 | + - `requestId`: a unique string ID value that was automatically generated to identify this request sequence |
| 79 | + - `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. |
| 80 | + |
| 81 | +The logic in the `payloadCreator` function may use any of these values as needed to calculate the result. |
| 82 | + |
| 83 | +## Return Value |
| 84 | + |
| 85 | +`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. |
| 86 | + |
| 87 | +When dispatched, the thunk will: |
| 88 | + |
| 89 | +- dispatch the `pending` action |
| 90 | +- call the `payloadCreator` callback and wait for the returned promise to settle |
| 91 | +- when the promise settles: |
| 92 | + - if the promise resolved successfully, dispatch the `fulfilled` action with the promise value as `action.payload` |
| 93 | + - if the promise failed, dispatch the `rejected` action with a serialized version of the error value as `action.error` |
| 94 | +- Return a fulfilled promise containing the final dispatched action (either the `fulfilled` or `rejected` action object) |
| 95 | + |
| 96 | +## Promise Lifecycle Actions |
| 97 | + |
| 98 | +`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`. |
| 99 | + |
| 100 | +The action creators will have these signatures: |
| 101 | + |
| 102 | +```ts |
| 103 | +interface SerializedError { |
| 104 | + name?: string |
| 105 | + message?: string |
| 106 | + code?: string |
| 107 | + stack?: string |
| 108 | +} |
| 109 | + |
| 110 | +interface PendingAction<ThunkArg> { |
| 111 | + type: string |
| 112 | + payload: undefined |
| 113 | + meta: { |
| 114 | + requestId: string |
| 115 | + arg: ThunkArg |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +interface FulfilledAction<ThunkArg, PromiseResult> { |
| 120 | + type: string |
| 121 | + payload: PromiseResult |
| 122 | + meta: { |
| 123 | + requestId: string |
| 124 | + arg: ThunkArg |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +interface RejectedAction<ThunkArg> { |
| 129 | + type: string |
| 130 | + payload: undefined |
| 131 | + error: SerializedError | any |
| 132 | + meta: { |
| 133 | + requestId: string |
| 134 | + arg: ThunkArg |
| 135 | + aborted: boolean |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +type Pending = <ThunkArg>( |
| 140 | + requestId: string, |
| 141 | + arg: ThunkArg |
| 142 | +) => PendingAction<ThunkArg> |
| 143 | + |
| 144 | +type Fulfilled = <ThunkArg, PromiseResult>( |
| 145 | + payload: PromiseResult, |
| 146 | + requestId: string, |
| 147 | + arg: ThunkArg |
| 148 | +) => FulfilledAction<ThunkArg, PromiseResult> |
| 149 | + |
| 150 | +type Rejected = <ThunkArg>( |
| 151 | + requestId: string, |
| 152 | + arg: ThunkArg |
| 153 | +) => RejectedAction<ThunkArg> |
| 154 | +``` |
| 155 | +
|
| 156 | +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: |
| 157 | +
|
| 158 | +```js {2,6,14,23} |
| 159 | +const reducer1 = createReducer(initialState, { |
| 160 | + [fetchUserById.fulfilled]: (state, action) => {} |
| 161 | +}) |
| 162 | + |
| 163 | +const reducer2 = createReducer(initialState, build => { |
| 164 | + builder.addCase(fetchUserById.fulfilled, (state, action) => {}) |
| 165 | +}) |
| 166 | + |
| 167 | +const reducer3 = createSlice({ |
| 168 | + name: 'users', |
| 169 | + initialState, |
| 170 | + reducers: {}, |
| 171 | + extraReducers: { |
| 172 | + [fetchUserById.fulfilled]: (state, action) => {} |
| 173 | + } |
| 174 | +}) |
| 175 | + |
| 176 | +const reducer4 = createSlice({ |
| 177 | + name: 'users', |
| 178 | + initialState, |
| 179 | + reducers: {}, |
| 180 | + extraReducers: builder => { |
| 181 | + builder.addCase(fetchUserById.fulfilled, (state, action) => {}) |
| 182 | + } |
| 183 | +}) |
| 184 | +``` |
| 185 | + |
| 186 | +## Handling Thunk Results |
| 187 | + |
| 188 | +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: |
| 189 | + |
| 190 | +```js |
| 191 | +const onClick = () => { |
| 192 | + dispatch(fetchUserById(userId)).then(() => { |
| 193 | + // do additional work |
| 194 | + }) |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +The thunks generated by `createAsyncThunk` will always return a resolved promise with either the `fulfilled` action object or `rejected` action object inside, as appropriate. |
| 199 | + |
| 200 | +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: |
| 201 | + |
| 202 | +```js |
| 203 | +import { unwrapResult } from '@reduxjs/toolkit' |
| 204 | + |
| 205 | +// in the component |
| 206 | +const onClick = () => { |
| 207 | + dispatch(fetchUserById(userId)) |
| 208 | + .then(unwrapResult) |
| 209 | + .then(originalPromiseResult => {}) |
| 210 | + .catch(serializedError => {}) |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +## Cancellation |
| 215 | + |
| 216 | +If you want to cancel your running thunk before it has finished, you can use the `abort` method of the promise returned by `dispatch(fetchUserById(userId))`. |
| 217 | + |
| 218 | +A real-life example of that would look like this: |
| 219 | + |
| 220 | +```ts |
| 221 | +function MyComponent(props: { userId: string }) { |
| 222 | + React.useEffect(() => { |
| 223 | + // Dispatching the thunk returns a promise |
| 224 | + const promise = dispatch(fetchUserById(props.userId)) |
| 225 | + return () => { |
| 226 | + // `createAsyncThunk` attaches an `abort()` method to the promise |
| 227 | + promise.abort() |
| 228 | + } |
| 229 | + }, [props.userId]) |
| 230 | +} |
| 231 | +``` |
| 232 | + |
| 233 | +After a thunk has been cancelled this way, it will dispatch (and return) a `"thunkName/rejected"` action with an `AbortError` on the `error` property. The thunk will not dispatch any further actions. |
| 234 | + |
| 235 | +Additionally, your `payloadCreator` can use the `AbortSignal` it is passed via `thunkApi.signal` to actually cancel a costly asynchronous action. |
| 236 | + |
| 237 | +The `fetch` api of modern browsers already comes with support for an `AbortSignal`: |
| 238 | + |
| 239 | +```ts |
| 240 | +const fetchUserById = createAsyncThunk( |
| 241 | + 'users/fetchById', |
| 242 | + async (userId, thunkAPI) => { |
| 243 | + const response = await fetch(`https://reqres.in/api/users/${userId}`, { |
| 244 | + signal: thunkAPI.signal |
| 245 | + }) |
| 246 | + return await response.json() |
| 247 | + } |
| 248 | +) |
| 249 | +``` |
| 250 | + |
| 251 | +### Checking Cancellation Status |
| 252 | + |
| 253 | +### Reading the Signal Value |
| 254 | + |
| 255 | +You can use the `signal.aborted` property to regularly check if the thunk has been aborted and in that case stop costly long-running work: |
| 256 | + |
| 257 | +```ts |
| 258 | +const readStream = createAsyncThunk('readStream', async (stream: ReadableStream, {signal}) => { |
| 259 | + const reader = stream.getReader(); |
| 260 | + |
| 261 | + let done = false; |
| 262 | + let result = ""; |
| 263 | + |
| 264 | + while (!done) { |
| 265 | + if (signal.aborted) { |
| 266 | + throw new Error("stop the work, this has been aborted!"); |
| 267 | + } |
| 268 | + const read = await reader.read(); |
| 269 | + result += read.value; |
| 270 | + done = read.done; |
| 271 | + } |
| 272 | + return result; |
| 273 | +} |
| 274 | +``` |
| 275 | +
|
| 276 | +#### Listening for Abort Events |
| 277 | +
|
| 278 | +You can also call `signal.addEventListener('abort', callback)` to have logic inside the thunk be notified when `promise.abort()` was called. |
| 279 | +
|
| 280 | +```ts |
| 281 | +const readStream = createAsyncThunk( |
| 282 | + 'readStream', |
| 283 | + (arg, { signal }) => |
| 284 | + new Promise((resolve, reject) => { |
| 285 | + signal.addEventListener('abort', () => { |
| 286 | + reject(new DOMException('Was aborted while running', 'AbortError')) |
| 287 | + }) |
| 288 | + |
| 289 | + startActionA(arg) |
| 290 | + .then(startActionB) |
| 291 | + .then(startActionC) |
| 292 | + .then(startActionD) |
| 293 | + .then(resolve) |
| 294 | + }) |
| 295 | +) |
| 296 | +``` |
| 297 | +
|
| 298 | +## Examples |
| 299 | +
|
| 300 | +Requesting a user by ID, with loading state, and only one request at a time: |
| 301 | +
|
| 302 | +```js |
| 303 | +import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit' |
| 304 | +import { userAPI } from './userAPI' |
| 305 | + |
| 306 | +const fetchUserById = createAsyncThunk( |
| 307 | + 'users/fetchByIdStatus', |
| 308 | + async (userId, { getState }) => { |
| 309 | + const { loading } = getState().users |
| 310 | + if (loading !== 'idle') { |
| 311 | + return |
| 312 | + } |
| 313 | + const response = await userAPI.fetchById(userId) |
| 314 | + return response.data |
| 315 | + } |
| 316 | +) |
| 317 | + |
| 318 | +const usersSlice = createSlice({ |
| 319 | + name: 'users', |
| 320 | + initialState: { |
| 321 | + entities: [], |
| 322 | + loading: 'idle', |
| 323 | + error: null |
| 324 | + }, |
| 325 | + reducers: {}, |
| 326 | + extraReducers: { |
| 327 | + [fetchUserById.pending]: (state, action) => { |
| 328 | + if (state.loading === 'idle') { |
| 329 | + state.loading = 'pending' |
| 330 | + } |
| 331 | + }, |
| 332 | + [fetchUserById.fulfilled]: (state, action) => { |
| 333 | + if (state.loading === 'pending') { |
| 334 | + state.loading = 'idle' |
| 335 | + state.push(action.payload) |
| 336 | + } |
| 337 | + }, |
| 338 | + [fetchUserById.rejected]: (state, action) => { |
| 339 | + if (state.loading === 'pending') { |
| 340 | + state.loading = 'idle' |
| 341 | + state.error = action.error |
| 342 | + } |
| 343 | + } |
| 344 | + } |
| 345 | +}) |
| 346 | + |
| 347 | +const UsersComponent = () => { |
| 348 | + const { users, loading, error } = useSelector(state => state.users) |
| 349 | + const dispatch = useDispatch() |
| 350 | + |
| 351 | + const fetchOneUser = async userId => { |
| 352 | + try { |
| 353 | + const resultAction = dispatch(fetchUserById(userId)) |
| 354 | + const user = unwrapResult(resultAction) |
| 355 | + showToast('success', `Fetched ${user.name}`) |
| 356 | + } catch (err) { |
| 357 | + showToast('error', `Fetch failed: ${err.message}`) |
| 358 | + } |
| 359 | + } |
| 360 | + |
| 361 | + // render UI here |
| 362 | +} |
| 363 | +``` |
0 commit comments