Skip to content

Add usage guide section on async logic and data fetching #401

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

Merged
merged 7 commits into from
Feb 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion docs/usage/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,4 +525,125 @@ This CodeSandbox example demonstrates the problem:

<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>

If you encounter this, you may need to restructure your code in a way that avoids the circular references.
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.

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.

## Asynchronous Logic and Data Fetching

### Using Middleware to Enable Async Logic

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.

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:

- Execute extra logic when any action is dispatched (such as logging the action and state)
- Pause, modify, delay, replace, or halt dispatched actions
- Write extra code that has access to `dispatch` and `getState`
- 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

[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.

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:

- [`redux-thunk`](https://github.com/reduxjs/redux-thunk), which lets you write plain functions that may contain async logic directly
- [`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
- [`redux-observable`](https://github.com/redux-observable/redux-observable/), which uses the RxJS observable library to create chains of functions that process actions

[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).

**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.

**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.

### Defining Async Logic in Slices

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.

:::note
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.
:::

Thunks typically dispatch plain actions, such as `dispatch(dataLoaded(response.data))`.

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.

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.

A typical slice file that includes thunks would look like this:

```js
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: []
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
}
}
})

// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions

// Define a thunk that dispatches those action creators
const fetchUsers = () => async dispatch => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
```

### Redux Data Fetching Patterns

Data fetching logic for Redux typically follows a predictable pattern:

- 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.
- The async request is made
- 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.

These steps are not required, but are [recommended in the Redux tutorials as a suggested pattern](https://redux.js.org/advanced/async-actions).

A typical implementation might look like:

```js
const getRepoDetailsStarted = () => ({
type: "repoDetails/fetchStarted"
})
const getRepoDetailsSuccess = (repoDetails) => {
type: "repoDetails/fetchSucceeded",
payload: repoDetails
}
const getRepoDetailsFailed = (error) => {
type: "repoDetails/fetchFailed",
error
}
const fetchIssuesCount = (org, repo) => async dispatch => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}
```

:::note
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.
:::

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`.
100 changes: 99 additions & 1 deletion website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,27 @@
--ifm-code-font-size: 95%;
--ifm-code-border-radius: 3px;
--ifm-code-background: rgba(27, 31, 35, 0.05);
--ifm-link-color: blue;

--ifm-blockquote-color: #ecf4f9;
--ifm-blockquote-color-dark: #cbddea;

--ifm-pre-background: rgb(39, 40, 34);

--ra-admonition-color: #ecf4f9;
--ra-admonition-color-dark: #2a98b9;

--ra-admonition-color-important: #2a98b9;

--ra-admonition-color-success: #f1fdf9;
--ra-admonition-color-success-dark: #00bf88;

--ra-admonition-color-caution: #fffbf5;
--ra-admonition-color-caution-dark: #f0ad4e;

--ra-admonition-color-error: #fff2f2;
--ra-admonition-color-error-dark: #d9534f;

--ra-admonition-icon-color: black !important;
}

@media screen and (max-width: 996px) {
Expand All @@ -31,13 +51,30 @@
}
}

blockquote {
color: var(--ifm-font-base-color);
background-color: var(--ifm-blockquote-color);
border-left: 6px solid var(--ifm-blockquote-color-dark);
border-radius: var(--ifm-global-radius);
}

.docusaurus-highlight-code-line {
background-color: rgb(72, 77, 91);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}

code {
background-color: var(--ifm-color-emphasis-300);
border-radius: 0.2rem;
}

a code,
code a {
color: inherit;
}

a.contents__link > code {
color: inherit;
}
Expand Down Expand Up @@ -65,3 +102,64 @@ a:visited {
.pagination-nav__link:hover * {
color: white;
}

.menu__link {
font-weight: normal;
}

.menu__link--sublist {
color: var(--ifm-font-base-color) !important;
font-weight: var(--ifm-font-weight-semibold);
}

.menu .menu__link.menu__link--sublist:after {
transform: rotateZ(180deg);
-webkit-transition: -webkit-transform 0.2s linear;
transition: -webkit-transform 0.2s linear;
transition-property: transform, -webkit-transform;
transition-duration: 0.2s, 0.2s;
transition-timing-function: linear, linear;
transition-delay: 0s, 0s;
transition: transform 0.2s linear;
transition: transform 0.2s linear, -webkit-transform 0.2s linear;
}

.menu .menu__list-item.menu__list-item--collapsed .menu__link--sublist:after {
transform: rotateZ(90deg);
}

.admonition {
color: var(--ifm-font-base-color);
border-radius: var(--ifm-global-radius);
border-left: 6px solid var(--ra-admonition-color-dark);
}

.admonition.admonition-note,
.admonition.admonition-info,
.admonition.admonition-important,
.admonition.admonition-secondary {
--ra-admonition-color: #ecf4f9;
background-color: var(--ra-admonition-color);
}

.admonition.admonition-success,
.admonition.admonition-tip {
background-color: var(--ra-admonition-color-success);
border-left-color: var(--ra-admonition-color-success-dark);
}

.admonition.admonition-caution {
background-color: var(--ra-admonition-color-caution);
border-left-color: var(--ra-admonition-color-caution-dark);
}

.admonition.admonition-warning,
.admonition.admonition-danger {
background-color: var(--ra-admonition-color-error);
border-left-color: var(--ra-admonition-color-error-dark);
}

.admonition .admonition-icon svg {
fill: black;
stroke: black;
}