Skip to content

Commit 2e420ef

Browse files
committed
Add usage guide content on async logic and data fetching
1 parent 9ef92f0 commit 2e420ef

File tree

1 file changed

+122
-1
lines changed

1 file changed

+122
-1
lines changed

docs/usage/usage-guide.md

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,125 @@ This CodeSandbox example demonstrates the problem:
525525

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

528-
If you encounter this, you may need to restructure your code in a way that avoids the circular references.
528+
If you encounter this, you may need to restructure your code in a way that avoids the circular references. This will usually require extracting shared code to a separate common file that both modules can import and use. In this case, you might define some common action types in a separate file using `createAction`, import those action creators into each slice file, and handle them using the `extraReducers` argument.
529+
530+
The article [How to fix circular dependency issues in JS](https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de) has additional info and examples that can help with this issue.
531+
532+
## Asynchronous Logic and Data Fetching
533+
534+
### Using Middleware to Enable Async Logic
535+
536+
By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.
537+
538+
But, what if you want to have async logic interact with the store by dispatching or checking the current store state? That's where [Redux middleware](https://redux.js.org/advanced/middleware) come in. They extend the store, and allow you to:
539+
540+
- Execute extra logic when any action is dispatched (such as logging the action and state)
541+
- Pause, modify, delay, replace, or halt dispatched actions
542+
- Write extra code that has access to `dispatch` and `getState`
543+
- Teach `dispatch` how to accept other values besides plain action objects, such as functions and promises, by intercepting them and dispatching real action objects instead
544+
545+
[The most common reason to use middleware is to allow different kinds of async logic to interact with the store](https://redux.js.org/faq/actions#how-can-i-represent-side-effects-such-as-ajax-calls-why-do-we-need-things-like-action-creators-thunks-and-middleware-to-do-async-behavior). This allows you to write code that can dispatch actions and check the store state, while keeping that logic separate from your UI.
546+
547+
There are many kinds of async middleware for Redux, and each lets you write your logic using different syntax. The most common async middleware are:
548+
549+
- [`redux-thunk`](https://github.com/reduxjs/redux-thunk), which lets you write plain functions that may contain async logic directly
550+
- [`redux-saga`](https://github.com/redux-saga/redux-saga), which uses generator functions that return descriptions of behavior so they can be executed by the middleware
551+
- [`redux-observable`](https://github.com/redux-observable/redux-observable/), which uses the RxJS observable library to create chains of functions that process actions
552+
553+
[Each of these libraries has different use cases and tradeoffs](https://redux.js.org/faq/actions#what-async-middleware-should-i-use-how-do-you-decide-between-thunks-sagas-observables-or-something-else).
554+
555+
**We recommend [using the Redux Thunk middleware as the standard approach](https://github.com/reduxjs/redux-thunk)**, as it is sufficient for most typical use cases (such as basic AJAX data fetching). In addition, use of the `async/await` syntax in thunks makes them easier to read.
556+
557+
**The Redux Toolkit `configureStore` function [automatically sets up the thunk middleware by default](../api/getDefaultMiddleware.md)**, so you can immediately start writing thunks as part of your application code.
558+
559+
### Defining Async Logic in Slices
560+
561+
Redux Toolkit does not currently provide any special APIs or syntax for writing thunk functions. In particular, **they cannot be defined as part of a `createSlice()` call**. You have to write them separate from the reducer logic, exactly the same as with plain Redux code.
562+
563+
:::note
564+
The upcoming [Redux Toolkit v1.3.0 release](https://github.com/reduxjs/redux-toolkit/issues/373), currently in alpha testing, will include [a `createAsyncThunk` API ](https://deploy-preview-374--redux-starter-kit-docs.netlify.com/api/createAsyncThunk) that simplifies the process of writing thunks for async logic.
565+
:::
566+
567+
Thunks typically dispatch plain actions, such as `dispatch(dataLoaded(response.data))`.
568+
569+
Many Redux apps have structured their code using a "folder-by-type" approach. In that structure, thunk action creators are usually defined in an "actions" file, alongside the plain action creators.
570+
571+
Because we don't have separate "actions" files, **it makes sense to write these thunks directly in our "slice" files**. That way, they have access to the plain action creators from the slice, and it's easy to find where the thunk function lives.
572+
573+
A typical slice file that includes thunks would look like this:
574+
575+
```js
576+
// First, define the reducer and action creators via `createSlice`
577+
const usersSlice = createSlice({
578+
name: 'users',
579+
initialState: {
580+
loading: 'idle',
581+
users: []
582+
},
583+
reducers: {
584+
usersLoading(state, action) {
585+
// Use a "state machine" approach for loading state instead of booleans
586+
if (state.loading === 'idle') {
587+
state.loading = 'pending'
588+
}
589+
},
590+
usersReceived(state, action) {
591+
if (state.loading === 'pending') {
592+
state.loading = 'idle'
593+
state.users = action.payload
594+
}
595+
}
596+
}
597+
})
598+
599+
// Destructure and export the plain action creators
600+
export const { usersLoading, usersReceived } = usersSlice.actions
601+
602+
// Define a thunk that dispatches those action creators
603+
const fetchUsers = () => async dispatch => {
604+
dispatch(usersLoading())
605+
const response = await usersAPI.fetchAll()
606+
dispatch(usersReceived(response.data))
607+
}
608+
```
609+
610+
### Redux Data Fetching Patterns
611+
612+
Data fetching logic for Redux typically follows a predictable pattern:
613+
614+
- 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.
615+
- The async request is made
616+
- 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.
617+
618+
These steps are not required, but are [recommended in the Redux tutorials as a suggested pattern](https://redux.js.org/advanced/async-actions).
619+
620+
A typical implementation might look like:
621+
622+
```js
623+
const getRepoDetailsStarted = () => ({
624+
type: "repoDetails/fetchStarted"
625+
})
626+
const getRepoDetailsSuccess = (repoDetails) => {
627+
type: "repoDetails/fetchSucceeded",
628+
payload: repoDetails
629+
}
630+
const getRepoDetailsFailed = (error) => {
631+
type: "repoDetails/fetchFailed",
632+
error
633+
}
634+
const fetchIssuesCount = (org, repo) => async dispatch => {
635+
dispatch(getRepoDetailsStarted())
636+
try {
637+
const repoDetails = await getRepoDetails(org, repo)
638+
dispatch(getRepoDetailsSuccess(repoDetails))
639+
} catch (err) {
640+
dispatch(getRepoDetailsFailed(err.toString()))
641+
}
642+
}
643+
```
644+
645+
:::note
646+
The upcoming [`createAsyncThunk` API in RTK v1.3.0](https://deploy-preview-374--redux-starter-kit-docs.netlify.com/api/createAsyncThunk) will automate this process by generating the action types and dispatching them automatically for you.
647+
:::
648+
649+
It's up to you to write reducer logic that handles these action types. We recommend [treating your loading state as a "state machine"](https://redux.js.org/style-guide/style-guide#treat-reducers-as-state-machines) instead of writing boolean flags like `isLoading`.

0 commit comments

Comments
 (0)