diff --git a/README.md b/README.md index 1d4d7f4..1d8b6b2 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,147 @@ # Redux Thunk -Thunk [middleware](https://redux.js.org/advanced/middleware) for Redux. +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 ``` -## Note on 2.x Update +The thunk middleware is the default export. -Most tutorials today assume that you're using Redux Thunk 1.x. You may run into -issues when you run their code with 2.x. **If you use Redux Thunk 2.x in -CommonJS environment, -[don’t forget to add `.default` to your import](https://github.com/reduxjs/redux-thunk/releases/tag/v2.0.0):** +
+More Details: Importing the thunk middleware -```diff -- const ReduxThunk = require('redux-thunk') -+ const ReduxThunk = require('redux-thunk').default +If you're using ES modules: + +```js +import thunk from 'redux-thunk' // no changes here 😀 ``` -If you used ES modules, you’re already all good: +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): -```js -import ReduxThunk from 'redux-thunk'; // no changes here 😀 +```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): ```js -const ReduxThunk = window.ReduxThunk.default; +const ReduxThunk = window.ReduxThunk ``` -As you can see, it also requires `.default` at the end. +
+ +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' + +// Note: this API requires redux@>=3.1.0 +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? @@ -51,6 +155,10 @@ 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 uesful 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 @@ -80,7 +188,7 @@ it is used by default in our ## Motivation -Redux Thunk [middleware](https://redux.js.org/advanced/middleware) +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 @@ -89,21 +197,21 @@ a certain condition is met. The inner function receives the store methods An action creator that returns a function to perform asynchronous dispatch: ```js -const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; +const INCREMENT_COUNTER = 'INCREMENT_COUNTER' function increment() { return { - type: INCREMENT_COUNTER, - }; + type: INCREMENT_COUNTER + } } function incrementAsync() { - return (dispatch) => { + return dispatch => { setTimeout(() => { // Yay! Can invoke sync or async actions with `dispatch` - dispatch(increment()); - }, 1000); - }; + dispatch(increment()) + }, 1000) + } } ``` @@ -112,14 +220,14 @@ An action creator that returns a function to perform conditional dispatch: ```js function incrementIfOdd() { return (dispatch, getState) => { - const { counter } = getState(); + const { counter } = getState() if (counter % 2 === 0) { - return; + return } - dispatch(increment()); - }; + dispatch(increment()) + } } ``` @@ -131,35 +239,17 @@ expression to delay its evaluation. ```js // calculation of 1 + 2 is immediate // x === 3 -let x = 1 + 2; +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; +let foo = () => 1 + 2 ``` The term [originated](https://en.wikipedia.org/wiki/Thunk#cite_note-1) as a humorous past-tense version of "think". -## Installation - -```bash -npm install redux-thunk -``` - -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'; - -// Note: this API requires redux@>=3.1.0 -const store = createStore(rootReducer, applyMiddleware(thunk)); -``` - ## Composition Any return value from the inner function will be available as the return value @@ -168,15 +258,15 @@ 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'; +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)); +const store = createStore(rootReducer, applyMiddleware(thunk)) function fetchSecretSauce() { - return fetch('https://www.google.com/search?q=secret+sauce'); + return fetch('https://www.google.com/search?q=secret+sauce') } // These are the normal action creators you have seen so far. @@ -187,8 +277,8 @@ function makeASandwich(forPerson, secretSauce) { return { type: 'MAKE_SANDWICH', forPerson, - secretSauce, - }; + secretSauce + } } function apologize(fromPerson, toPerson, error) { @@ -196,19 +286,19 @@ function apologize(fromPerson, toPerson, error) { type: 'APOLOGIZE', fromPerson, toPerson, - error, - }; + error + } } function withdrawMoney(amount) { return { type: 'WITHDRAW', - amount, - }; + amount + } } // Even without middleware, you can dispatch an action: -store.dispatch(withdrawMoney(100)); +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? @@ -222,37 +312,37 @@ function makeASandwichWithSecretSauce(forPerson) { // 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 function (dispatch) { return fetchSecretSauce().then( - (sauce) => dispatch(makeASandwich(forPerson, sauce)), - (error) => dispatch(apologize('The Sandwich Shop', forPerson, error)), - ); - }; + 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')); +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!'); -}); + 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) { + 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(); + return Promise.resolve() } // We can dispatch both plain object actions and other thunks, @@ -262,18 +352,18 @@ function makeSandwichesForEverybody() { .then(() => Promise.all([ dispatch(makeASandwichWithSecretSauce('Me')), - dispatch(makeASandwichWithSecretSauce('My wife')), - ]), + dispatch(makeASandwichWithSecretSauce('My wife')) + ]) ) .then(() => dispatch(makeASandwichWithSecretSauce('Our kids'))) .then(() => dispatch( getState().myMoney > 42 ? withdrawMoney(42) - : apologize('Me', 'The Sandwich Shop'), - ), - ); - }; + : apologize('Me', 'The Sandwich Shop') + ) + ) + } } // This is very useful for server side rendering, because I can wait @@ -282,73 +372,34 @@ function makeSandwichesForEverybody() { store .dispatch(makeSandwichesForEverybody()) .then(() => - response.send(ReactDOMServer.renderToString()), - ); + response.send(ReactDOMServer.renderToString()) + ) // 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'; +import { connect } from 'react-redux' +import { Component } from 'react' class SandwichShop extends Component { componentDidMount() { - this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)); + this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } componentDidUpdate(prevProps) { if (prevProps.forPerson !== this.props.forPerson) { - this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)); + this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } } render() { - return

{this.props.sandwiches.join('mustard')}

; + return

{this.props.sandwiches.join('mustard')}

} } -export default connect((state) => ({ - sandwiches: state.sandwiches, -}))(SandwichShop); -``` - -## Injecting a Custom Argument - -Since 2.1.0, Redux Thunk supports injecting a custom argument using the -`withExtraArgument` function: - -```js -const store = createStore( - reducer, - applyMiddleware(thunk.withExtraArgument(api)), -); - -// later -function fetchUser(id) { - return (dispatch, getState, api) => { - // you can use api here - }; -} -``` - -To pass multiple things, just wrap them in a single object. -Using ES2015 shorthand property names can make this more concise. - -```js -const api = "http://www.example.com/sandwiches/"; -const whatever = 42; - -const store = createStore( - reducer, - applyMiddleware(thunk.withExtraArgument({ api, whatever })), -); - -// later -function fetchUser(id) { - return (dispatch, getState, { api, whatever }) => { - // you can use api and something else here - }; -} +export default connect(state => ({ + sandwiches: state.sandwiches +}))(SandwichShop) ``` ## License