A Dummy's Guide to Redux and Thunk in React
If, like me, you've read the Redux docs, watched Dan's videos, done Wes' course and still not quite grasped how to use Redux, hopefully this will help.
It took me a few attempts at using Redux before it clicked, so I thought I'd write down the process of converting an existing application that fetches JSON to use Redux and Redux Thunk. If you don't know what Thunk is, don't worry too much, but we'll use it to make asynchronous calls in the "Redux way".
This tutorial assumes you have a basic grasp of React and ES6/2015, but it should hopefully be easy enough to follow along regardless.
The non-Redux way
Let's start with creating a React component in components/ItemList.js
to fetch and display a list of items.
Laying the foundations
First we'll setup a static component with a state
that contains various items
to output, and 2 boolean states to render something different when it's loading or errored respectively.
import React, { Component } from 'react';
class ItemList extends Component {
constructor() {
super();
this.state = {
items: [
{
id: 1,
label: 'List item 1'
},
{
id: 2,
label: 'List item 2'
},
{
id: 3,
label: 'List item 3'
},
{
id: 4,
label: 'List item 4'
}
],
hasErrored: false,
isLoading: false
};
}
render() {
if (this.state.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.state.isLoading) {
return <p>Loading…</p>;
}
return (
<ul>
{this.state.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
export default ItemList;
It may not seem like much, but this is a good start.
When rendered, the component should output 4 list items, but if you were to set isLoading
or hasErrored
to true
, a relevant <p></p>
would be output instead.
Making it dynamic
Hard-coding the items doesn't make for a very useful component, so let's fetch the items
from a JSON API, which will also allow us to set isLoading
and hasErrored
as appropriate.
The response will be identical to our hard-coded list of items, but in the real world, you could pull in a list of best-selling books, latest blog posts, or whatever suits your application.
To fetch the items, we're going to use the aptly named Fetch API. Fetch makes making requests much easier than the classic XMLHttpRequest and returns a promise of the resolved response (which is important to Thunk). Fetch isn't available in all browsers, so you'll need to add it as a dependency to your project with:
npm install whatwg-fetch --save
The conversion is actually quite simple.
- First we'll set our initial
items
to an empty array[]
Now we'll add a method to fetch the data and set the loading and error states:
fetchData(url) { this.setState({ isLoading: true }); fetch(url) .then((response) => { if (!response.ok) { throw Error(response.statusText); } this.setState({ isLoading: false }); return response; }) .then((response) => response.json()) .then((items) => this.setState({ items })) // ES6 property value shorthand for { items: items } .catch(() => this.setState({ hasErrored: true })); }
Then we'll call it when the component mounts:
componentDidMount() { this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items'); }
Which leaves us with (unchanged lines omitted):
class ItemList extends Component {
constructor() {
this.state = {
items: [],
};
}
fetchData(url) {
this.setState({ isLoading: true });
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
this.setState({ isLoading: false });
return response;
})
.then((response) => response.json())
.then((items) => this.setState({ items }))
.catch(() => this.setState({ hasErrored: true }));
}
componentDidMount() {
this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
}
}
And that's it. Your component now fetches the items
from a REST endpoint! You should hopefully see "Loading…"
appear briefly before the 4 list items. If you pass in a broken URL to fetchData
you should see our error message.
However, in reality, a component shouldn't include logic to fetch data, and data shouldn't be stored in a component's state, so this is where Redux comes in.
Converting to Redux
To start, we need to add Redux, React Redux and Redux Thunk as dependencies of our project so we can use them. We can do that with:
npm install redux react-redux redux-thunk --save
Understanding Redux
There are a few core principles to Redux which we need to understand:
- There is 1 global state object that manages the state for your entire application. In this example, it will behave identically to our initial component's state. It is the single source of truth.
- The only way to modify the state is through emitting an action, which is an object that describes what should change. Action Creators are the functions that are
dispatch
ed to emit a change – all they do isreturn
an action. - When an action is
dispatch
ed, a Reducer is the function that actually changes the state appropriate to that action – or returns the existing state if the action is not applicable to that reducer. - Reducers are "pure functions". They should not have any side-effects nor mutate the state – they must return a modified copy.
- Individual reducers are combined into a single
rootReducer
to create the discrete properties of the state. - The Store is the thing that brings it all together: it represents the state by using the
rootReducer
, any middleware (Thunk in our case), and allows you to actuallydispatch
actions. - For using Redux in React, the
<Provider />
component wraps the entire application and passes thestore
down to all children.
This should all become clearer as we start to convert our application to use Redux.
Designing our state
From the work we've already done, we know that our state needs to have 3 properties: items
, hasErrored
and isLoading
for this application to work as expected under all circumstances, which correlates to needing 3 unique actions.
Now, here is why Action Creators are different to Actions and do not necessarily have a 1:1 relationship: we need a fourth action creator that calls our 3 other action (creators) depending on the status of fetching the data. This fourth action creator is almost identical to our original fetchData()
method, but instead of directly setting the state with this.setState({ isLoading: true })
, we'll dispatch
an action to do the same: dispatch(isLoading(true))
.
Creating our actions
Let's create an actions/items.js
file to contain our action creators. We'll start with our 3 simple actions.
export function itemsHasErrored(bool) {
return {
type: 'ITEMS_HAS_ERRORED',
hasErrored: bool
};
}
export function itemsIsLoading(bool) {
return {
type: 'ITEMS_IS_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}
As mentioned before, action creators are functions that return an action. We export
each one so we can use them elsewhere in our codebase.
The first 2 action creators take a bool
(true
/false
) as their argument and return an object with a meaningful type
and the bool
assigned to the appropriate property.
The third, itemsFetchDataSuccess()
, will be called when the data has been successfully fetched, with the data passed to it as items
. Through the magic of ES6 property value shorthands, we'll return an object with a property called items
whose value will be the array of items
;
Note: that the value you use for type
and the name of the other property that is returned is important, because you will re-use them in your reducers
Now that we have the 3 actions which will represent our state, we'll convert our original component's fetchData
method to an itemsFetchData()
action creator.
By default, Redux action creators don't support asynchronous actions like fetching data, so here's where we utilise Redux Thunk. Thunk allows you to write action creators that return a function instead of an action. The inner function can receive the store methods dispatch
and getState
as parameters, but we'll just use dispatch
.
A real simple example would be to manually trigger itemsHasErrored()
after 5 seconds.
export function errorAfterFiveSeconds() {
// We return a function instead of an action object
return (dispatch) => {
setTimeout(() => {
// This function is able to dispatch other action creators
dispatch(itemsHasErrored(true));
}, 5000);
};
}
Now we know what a thunk is, we can write itemsFetchData()
.
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}
Creating our reducers
With our action creators defined, we now write reducers that take these actions and return a new state of our application.
Note: In Redux, all reducers get called regardless of the action, so inside each one you have to return the original state
if the action is not applicable.
Each reducer takes 2 parameters: the previous state (state
) and an action
object. We can also use an ES6 feature called default parameters to set the default initial state
.
Inside each reducer, we use a switch
statement to determine when an action.type
matches. While it may seem unnecessary in these simple reducers, your reducers could theoretically have a lot of conditions, so if
/else if
/else
will get messy fast.
If the action.type
matches, then we return the relevant property of action
. As mentioned earlier, the type
and action[propertyName]
is what was defined in your action creators.
OK, knowing this, let's create our items reducers in reducers/items.js
.
export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
Notice how each reducer is named after the resulting store's state property, with the action.type
not necessarily needing to correspond. The first 2 reducers hopefully make complete sense, but the last, items()
, is slightly different.
This is because it could have multiple conditions which would always return an array of items
: it could return all in the case of a fetch success, it could return a subset of items
after a delete action is dispatch
ed, or it could return an empty array if everything is deleted.
To re-iterate, every reducer will return a discrete property of the state, regardless of how many conditions are inside that reducer. That initially took me a while to get my head around.
With the individual reducers created, we need to combine them into a rootReducer
to create a single object.
Create a new file at reducers/index.js
.
import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading
});
We import each of the reducers from items.js
and export them with Redux's combineReducers()
. As our reducer names are identical to what we want to use for a store's property names, we can use the ES6 shorthand.
Notice how I intentionally prefixed my reducer names, so that when the application grows in complexity, I'm not constrained by having a "global" hasErrored
or isLoading
property. You may have many different features that could error or be in a loading state, so prefixing the imports and then exporting those will give your application's state greater granularity and flexibility. For example:
import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
import { posts, postsHasErrored, postsIsLoading } from './posts';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading,
posts,
postsHasErrored,
postsIsLoading
});
Alternatively, you could alias the methods on import
, but I prefer consistency across the board.
Configure the store and provide it to your app
This is pretty straightforward. Let's create store/configureStore.js
with:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}
Now change our app's index.js
to include <Provider />
, configureStore
, set up our store
and wrap our app (<ItemList />
) to pass the store
down as props
:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import ItemList from './components/ItemList';
const store = configureStore(); // You can also pass in an initialState here
render(
<Provider store={store}>
<ItemList />
</Provider>,
document.getElementById('app')
);
I know, it's taken quite a bit of effort to get to this stage, but with the set up complete, we can now modify our component to make use of what we've done.
Converting our component to use the Redux store and methods
Let's jump back in to components/ItemList.js
.
At the top of the file, import
what we need:
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
connect
is what allows us to connect a component to Redux's store, and itemsFetchData
is the action creator we wrote earlier. We only need to import this one action creator, as it handles dispatch
ing the other actions.
After our component's class
definition, we're going to map Redux's state and the dispatching of our action creator to props.
We create a function that accepts state
and then returns an object of props. In a simple component like this, I remove the prefixing for the has
/is
props as it's obvious that they're related to items
.
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
And then we need another function to be able to dispatch
our itemsFetchData()
action creator with a prop.
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
Again, I've removed the items
prefix from the returned object property. Here fetchData
is a function that accepts a url
parameter and returns dispatch
ing itemsFetchData(url)
.
Now, these 2 mapStateToProps()
and mapDispatchToProps()
don't do anything yet, so we need to change our final export
line to:
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
This connect
s our ItemList
to Redux while mapping the props for us to use.
The final step is to convert our component to use props
instead of state
, and to remove the leftovers.
- Delete the
constructor() {}
andfetchData() {}
methods as they're unnecessary now. - Change
this.fetchData()
incomponentDidMount()
tothis.props.fetchData()
. - Change
this.state.X
tothis.props.X
for.hasErrored
,.isLoading
and.items
.
Your component should now look like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
And that's it! The application now uses Redux and Redux Thunk to fetch and display the data!
That wasn't too difficult, was it?
And you're now a Redux master :D
What next?
I've put all of this code up on GitHub, with commits for each step. I want you to clone it, run it and understand it, then add the ability for the user to delete individual list items based on the item's index
.
I haven't yet really mentioned that in Redux, the state is immutable, which means you can't modify it, so have to return a new state in your reducers instead. The 3 reducers we wrote above were simple and "just worked", but deleting items from an array requires an approach that you may not be familiar with.
You can no longer use Array.prototype.splice()
to remove items from an array, as that will mutate the original array. Dan explains how to remove an element from an array in this video, but if you get stuck, you can check out (pun intended) the delete-items
branch for the solution.
I really hope that this has clarified the concept of Redux and Thunk and how you might go about converting an existing React application to use them. I know that writing this has solidified my understanding of it, so I'm very happy to have done it.
I'd still recommend reading the Redux docs, watching Dan's videos, and re-doing Wes' course as you should hopefully now be able to understand some of the other more complex and deeper principles.