407 lines
12 KiB
Markdown
407 lines
12 KiB
Markdown
# Redux Thunk
|
||
|
||
Thunk [middleware](https://redux.js.org/tutorials/fundamentals/part-4-store#middleware) for Redux. It allows writing functions with logic inside that can interact with a Redux store's `dispatch` and `getState` methods.
|
||
|
||
For complete usage instructions and useful patterns, see the [Redux docs **Writing Logic with Thunks** page](https://redux.js.org/usage/writing-logic-thunks).
|
||
|
||
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/reduxjs/redux-thunk/Tests)
|
||
[![npm version](https://img.shields.io/npm/v/redux-thunk.svg?style=flat-square)](https://www.npmjs.com/package/redux-thunk)
|
||
[![npm downloads](https://img.shields.io/npm/dm/redux-thunk.svg?style=flat-square)](https://www.npmjs.com/package/redux-thunk)
|
||
|
||
## Installation and Setup
|
||
|
||
### Redux Toolkit
|
||
|
||
If you're using [our official Redux Toolkit package](https://redux-toolkit.js.org) as recommended, there's nothing to install - RTK's `configureStore` API already adds the thunk middleware by default:
|
||
|
||
```js
|
||
import { configureStore } from '@reduxjs/toolkit'
|
||
|
||
import todosReducer from './features/todos/todosSlice'
|
||
import filtersReducer from './features/filters/filtersSlice'
|
||
|
||
const store = configureStore({
|
||
reducer: {
|
||
todos: todosReducer,
|
||
filters: filtersReducer
|
||
}
|
||
})
|
||
|
||
// The thunk middleware was automatically added
|
||
```
|
||
|
||
### Manual Setup
|
||
|
||
If you're using the basic Redux `createStore` API and need to set this up manually, first add the `redux-thunk` package:
|
||
|
||
```sh
|
||
npm install redux-thunk
|
||
|
||
yarn add redux-thunk
|
||
```
|
||
|
||
The thunk middleware is the default export.
|
||
|
||
<details>
|
||
<summary><b>More Details: Importing the thunk middleware</b></summary>
|
||
|
||
If you're using ES modules:
|
||
|
||
```js
|
||
import thunk from 'redux-thunk' // no changes here 😀
|
||
```
|
||
|
||
If you use Redux Thunk 2.x in a CommonJS environment,
|
||
[don’t forget to add `.default` to your import](https://github.com/reduxjs/redux-thunk/releases/tag/v2.0.0):
|
||
|
||
```diff
|
||
- const thunk = require('redux-thunk')
|
||
+ const thunk = require('redux-thunk').default
|
||
```
|
||
|
||
Additionally, since 2.x, we also support a
|
||
[UMD build](https://unpkg.com/redux-thunk/dist/redux-thunk.min.js) for use as a global script tag:
|
||
|
||
```js
|
||
const ReduxThunk = window.ReduxThunk
|
||
```
|
||
|
||
</details>
|
||
|
||
Then, to enable Redux Thunk, use
|
||
[`applyMiddleware()`](https://redux.js.org/api/applymiddleware):
|
||
|
||
```js
|
||
import { createStore, applyMiddleware } from 'redux'
|
||
import thunk from 'redux-thunk'
|
||
import rootReducer from './reducers/index'
|
||
|
||
const store = createStore(rootReducer, applyMiddleware(thunk))
|
||
```
|
||
|
||
### Injecting a Custom Argument
|
||
|
||
Since 2.1.0, Redux Thunk supports injecting a custom argument into the thunk middleware. This is typically useful for cases like using an API service layer that could be swapped out for a mock service in tests.
|
||
|
||
For Redux Toolkit, the `getDefaultMiddleware` callback inside of `configureStore` lets you pass in a custom `extraArgument`:
|
||
|
||
```js
|
||
import { configureStore } from '@reduxjs/toolkit'
|
||
import rootReducer from './reducer'
|
||
import { myCustomApiService } from './api'
|
||
|
||
const store = configureStore({
|
||
reducer: rootReducer,
|
||
middleware: getDefaultMiddleware =>
|
||
getDefaultMiddleware({
|
||
thunk: {
|
||
extraArgument: myCustomApiService
|
||
}
|
||
})
|
||
})
|
||
|
||
// later
|
||
function fetchUser(id) {
|
||
// The `extraArgument` is the third arg for thunk functions
|
||
return (dispatch, getState, api) => {
|
||
// you can use api here
|
||
}
|
||
}
|
||
```
|
||
|
||
If you need to pass in multiple values, combine them into a single object:
|
||
|
||
```js
|
||
const store = configureStore({
|
||
reducer: rootReducer,
|
||
middleware: getDefaultMiddleware =>
|
||
getDefaultMiddleware({
|
||
thunk: {
|
||
extraArgument: {
|
||
api: myCustomApiService,
|
||
otherValue: 42
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// later
|
||
function fetchUser(id) {
|
||
return (dispatch, getState, { api, otherValue }) => {
|
||
// you can use api and something else here
|
||
}
|
||
}
|
||
```
|
||
|
||
If you're setting up the store by hand, the default `thunk` export has an attached `thunk.withExtraArgument()` function that should be used to generate the correct thunk middleware:
|
||
|
||
```js
|
||
const store = createStore(
|
||
reducer,
|
||
applyMiddleware(thunk.withExtraArgument(api))
|
||
)
|
||
```
|
||
|
||
## Why Do I Need This?
|
||
|
||
With a plain basic Redux store, you can only do simple synchronous updates by
|
||
dispatching an action. Middleware extends the store's abilities, and lets you
|
||
write async logic that interacts with the store.
|
||
|
||
Thunks are the recommended middleware for basic Redux side effects logic,
|
||
including complex synchronous logic that needs access to the store, and simple
|
||
async logic like AJAX requests.
|
||
|
||
For more details on why thunks are useful, see:
|
||
|
||
- **Redux docs: Writing Logic with Thunks**
|
||
https://redux.js.org/usage/writing-logic-thunks
|
||
The official usage guide page on thunks. Covers why they exist, how the thunk middleware works, and useful patterns for using thunks.
|
||
|
||
- **Stack Overflow: Dispatching Redux Actions with a Timeout**
|
||
http://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559
|
||
Dan Abramov explains the basics of managing async behavior in Redux, walking
|
||
through a progressive series of approaches (inline async calls, async action
|
||
creators, thunk middleware).
|
||
|
||
- **Stack Overflow: Why do we need middleware for async flow in Redux?**
|
||
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
|
||
Dan Abramov gives reasons for using thunks and async middleware, and some
|
||
useful patterns for using thunks.
|
||
|
||
- **What the heck is a "thunk"?**
|
||
https://daveceddia.com/what-is-a-thunk/
|
||
A quick explanation for what the word "thunk" means in general, and for Redux
|
||
specifically.
|
||
|
||
- **Thunks in Redux: The Basics**
|
||
https://medium.com/fullstack-academy/thunks-in-redux-the-basics-85e538a3fe60
|
||
A detailed look at what thunks are, what they solve, and how to use them.
|
||
|
||
You may also want to read the
|
||
**[Redux FAQ entry on choosing which async middleware to use](https://redux.js.org/faq/actions#what-async-middleware-should-i-use-how-do-you-decide-between-thunks-sagas-observables-or-something-else)**.
|
||
|
||
While the thunk middleware is not directly included with the Redux core library,
|
||
it is used by default in our
|
||
**[`@reduxjs/toolkit` package](https://github.com/reduxjs/redux-toolkit)**.
|
||
|
||
## Motivation
|
||
|
||
Redux Thunk [middleware](https://redux.js.org/tutorials/fundamentals/part-4-store#middleware)
|
||
allows you to write action creators that return a function instead of an action.
|
||
The thunk can be used to delay the dispatch of an action, or to dispatch only if
|
||
a certain condition is met. The inner function receives the store methods
|
||
`dispatch` and `getState` as parameters.
|
||
|
||
An action creator that returns a function to perform asynchronous dispatch:
|
||
|
||
```js
|
||
const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
|
||
|
||
function increment() {
|
||
return {
|
||
type: INCREMENT_COUNTER
|
||
}
|
||
}
|
||
|
||
function incrementAsync() {
|
||
return dispatch => {
|
||
setTimeout(() => {
|
||
// Yay! Can invoke sync or async actions with `dispatch`
|
||
dispatch(increment())
|
||
}, 1000)
|
||
}
|
||
}
|
||
```
|
||
|
||
An action creator that returns a function to perform conditional dispatch:
|
||
|
||
```js
|
||
function incrementIfOdd() {
|
||
return (dispatch, getState) => {
|
||
const { counter } = getState()
|
||
|
||
if (counter % 2 === 0) {
|
||
return
|
||
}
|
||
|
||
dispatch(increment())
|
||
}
|
||
}
|
||
```
|
||
|
||
## What’s a thunk?!
|
||
|
||
A [thunk](https://en.wikipedia.org/wiki/Thunk) is a function that wraps an
|
||
expression to delay its evaluation.
|
||
|
||
```js
|
||
// calculation of 1 + 2 is immediate
|
||
// x === 3
|
||
let x = 1 + 2
|
||
|
||
// calculation of 1 + 2 is delayed
|
||
// foo can be called later to perform the calculation
|
||
// foo is a thunk!
|
||
let foo = () => 1 + 2
|
||
```
|
||
|
||
The term [originated](https://en.wikipedia.org/wiki/Thunk#cite_note-1) as a
|
||
humorous past-tense version of "think".
|
||
|
||
## Composition
|
||
|
||
Any return value from the inner function will be available as the return value
|
||
of `dispatch` itself. This is convenient for orchestrating an asynchronous
|
||
control flow with thunk action creators dispatching each other and returning
|
||
Promises to wait for each other’s completion:
|
||
|
||
```js
|
||
import { createStore, applyMiddleware } from 'redux'
|
||
import thunk from 'redux-thunk'
|
||
import rootReducer from './reducers'
|
||
|
||
// Note: this API requires redux@>=3.1.0
|
||
const store = createStore(rootReducer, applyMiddleware(thunk))
|
||
|
||
function fetchSecretSauce() {
|
||
return fetch('https://www.google.com/search?q=secret+sauce')
|
||
}
|
||
|
||
// These are the normal action creators you have seen so far.
|
||
// The actions they return can be dispatched without any middleware.
|
||
// However, they only express “facts” and not the “async flow”.
|
||
|
||
function makeASandwich(forPerson, secretSauce) {
|
||
return {
|
||
type: 'MAKE_SANDWICH',
|
||
forPerson,
|
||
secretSauce
|
||
}
|
||
}
|
||
|
||
function apologize(fromPerson, toPerson, error) {
|
||
return {
|
||
type: 'APOLOGIZE',
|
||
fromPerson,
|
||
toPerson,
|
||
error
|
||
}
|
||
}
|
||
|
||
function withdrawMoney(amount) {
|
||
return {
|
||
type: 'WITHDRAW',
|
||
amount
|
||
}
|
||
}
|
||
|
||
// Even without middleware, you can dispatch an action:
|
||
store.dispatch(withdrawMoney(100))
|
||
|
||
// But what do you do when you need to start an asynchronous action,
|
||
// such as an API call, or a router transition?
|
||
|
||
// Meet thunks.
|
||
// A thunk in this context is a function that can be dispatched to perform async
|
||
// activity and can dispatch actions and read state.
|
||
// This is an action creator that returns a thunk:
|
||
function makeASandwichWithSecretSauce(forPerson) {
|
||
// We can invert control here by returning a function - the "thunk".
|
||
// When this function is passed to `dispatch`, the thunk middleware will intercept it,
|
||
// and call it with `dispatch` and `getState` as arguments.
|
||
// This gives the thunk function the ability to run some logic, and still interact with the store.
|
||
return function (dispatch) {
|
||
return fetchSecretSauce().then(
|
||
sauce => dispatch(makeASandwich(forPerson, sauce)),
|
||
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
|
||
)
|
||
}
|
||
}
|
||
|
||
// Thunk middleware lets me dispatch thunk async actions
|
||
// as if they were actions!
|
||
|
||
store.dispatch(makeASandwichWithSecretSauce('Me'))
|
||
|
||
// It even takes care to return the thunk’s return value
|
||
// from the dispatch, so I can chain Promises as long as I return them.
|
||
|
||
store.dispatch(makeASandwichWithSecretSauce('My partner')).then(() => {
|
||
console.log('Done!')
|
||
})
|
||
|
||
// In fact I can write action creators that dispatch
|
||
// actions and async actions from other action creators,
|
||
// and I can build my control flow with Promises.
|
||
|
||
function makeSandwichesForEverybody() {
|
||
return function (dispatch, getState) {
|
||
if (!getState().sandwiches.isShopOpen) {
|
||
// You don’t have to return Promises, but it’s a handy convention
|
||
// so the caller can always call .then() on async dispatch result.
|
||
|
||
return Promise.resolve()
|
||
}
|
||
|
||
// We can dispatch both plain object actions and other thunks,
|
||
// which lets us compose the asynchronous actions in a single flow.
|
||
|
||
return dispatch(makeASandwichWithSecretSauce('My Grandma'))
|
||
.then(() =>
|
||
Promise.all([
|
||
dispatch(makeASandwichWithSecretSauce('Me')),
|
||
dispatch(makeASandwichWithSecretSauce('My wife'))
|
||
])
|
||
)
|
||
.then(() => dispatch(makeASandwichWithSecretSauce('Our kids')))
|
||
.then(() =>
|
||
dispatch(
|
||
getState().myMoney > 42
|
||
? withdrawMoney(42)
|
||
: apologize('Me', 'The Sandwich Shop')
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
// This is very useful for server side rendering, because I can wait
|
||
// until data is available, then synchronously render the app.
|
||
|
||
store
|
||
.dispatch(makeSandwichesForEverybody())
|
||
.then(() =>
|
||
response.send(ReactDOMServer.renderToString(<MyApp store={store} />))
|
||
)
|
||
|
||
// I can also dispatch a thunk async action from a component
|
||
// any time its props change to load the missing data.
|
||
|
||
import { connect } from 'react-redux'
|
||
import { Component } from 'react'
|
||
|
||
class SandwichShop extends Component {
|
||
componentDidMount() {
|
||
this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson))
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
if (prevProps.forPerson !== this.props.forPerson) {
|
||
this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson))
|
||
}
|
||
}
|
||
|
||
render() {
|
||
return <p>{this.props.sandwiches.join('mustard')}</p>
|
||
}
|
||
}
|
||
|
||
export default connect(state => ({
|
||
sandwiches: state.sandwiches
|
||
}))(SandwichShop)
|
||
```
|
||
|
||
## License
|
||
|
||
MIT
|