If a component needs data in order to render, and you want to fetch that data with Redux and keep it in the Redux store, when is the best time to make that API call?
tl;dr – Kick off the action in the componentDidMount
lifecycle hook
Making API Calls with Redux
Let’s imagine you want to display a list of products. You’ve got a backend API that answers to GET /products
, so you create a Redux action to do the fetching:
export function fetchProducts() {
return dispatch => {
dispatch(fetchProductsBegin());
return fetch("/products")
.then(handleErrors)
.then(res => res.json())
.then(json => {
dispatch(fetchProductsSuccess(json.products));
return json.products;
})
.catch(error => dispatch(fetchProductsFailure(error)));
};
}
// Handle HTTP errors since fetch won't.
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
Side note: fetch()
does not throw for HTTP errors like 404s. This is really confusing if you’re used to something like axios. Read here for more about fetch and error handling.
Use redux-thunk to Fetch Data with Redux
Normally, actions must be plain objects. Returning a function, like we’re doing in fetchProducts
, is outside the ordinary and Redux won’t allow it. Not without some help, anyway.
That’s where redux-thunk
comes in. It’s a middleware which we can add to Redux to effectively teach it how to deal with new kinds of actions. (you can read more about what a thunk is if you’re curious)
You can install redux-thunk with npm install redux-thunk
, and then it takes just a couple lines of setup to extend the Redux store with this new middleware.
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
The important thing to note about setting up redux-thunk
is that you have to wrap it in applyMiddleware
before passing it to Redux. There’s also that rootReducer
thing – we’ll see where that comes from later on.
This code could live in index.js
, or it could be tucked away in its own file (store.js
is a nice name). Redux doesn’t care where you put your files. Glom them together if you want. As long as you’ve got a store, and it’s provided to your app with a Provider
(from react-redux
), you’re all set.
How to Name Your Redux Actions
Redux actions that fetch data usually come in triplets: BEGIN, SUCCESS, FAILURE. This isn’t a requirement, it’s just a convention.
Before you start the API call, you dispatch the BEGIN action.
Then after the call succeeds, you dispatch SUCCESS with the data. If it failed instead, you dispatch FAILURE with the error.
Sometimes the last one is called ERROR instead. It’s not a big deal, just be consistent about it. (like I wasn’t, when I wrote this post the first time).
Careful: Dispatching an ERROR action and handling a FAILURE will lead to no end of hair pulling as you trace through your code, realizing the action is dispatching correctly but the data is never updating. Learn from my mistakes :)
This BEGIN/SUCCESS/FAILURE pattern is nice because it gives you hooks to keep track of what’s happening – say, by setting a “loading” flag true
in response to the BEGIN action, and then false
after SUCCESS or FAILURE. Here’s what those actions look like:
export const FETCH_PRODUCTS_BEGIN = 'FETCH_PRODUCTS_BEGIN';
export const FETCH_PRODUCTS_SUCCESS = 'FETCH_PRODUCTS_SUCCESS';
export const FETCH_PRODUCTS_FAILURE = 'FETCH_PRODUCTS_FAILURE';
export const fetchProductsBegin = () => ({
type: FETCH_PRODUCTS_BEGIN
});
export const fetchProductsSuccess = products => ({
type: FETCH_PRODUCTS_SUCCESS,
payload: { products }
});
export const fetchProductsFailure = error => ({
type: FETCH_PRODUCTS_FAILURE,
payload: { error }
});
And then we’ll have the reducer save the products into the Redux store when it receives the FETCH_PRODUCTS_SUCCESS
action. It’ll also set a loading
flag to true when the fetch begins, and false when it finishes or fails.
import {
FETCH_PRODUCTS_BEGIN,
FETCH_PRODUCTS_SUCCESS,
FETCH_PRODUCTS_FAILURE
} from './productActions';
const initialState = {
items: [],
loading: false,
error: null
};
export default function productReducer(state = initialState, action) {
switch(action.type) {
case FETCH_PRODUCTS_BEGIN:
// Mark the state as "loading" so we can show a spinner or something
// Also, reset any errors. We're starting fresh.
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
// All done: set loading "false".
// Also, replace the items with the ones from the server
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_FAILURE:
// The request failed. It's done. So set loading to "false".
// Save the error, so we can display it somewhere.
// Since it failed, we don't have items to display anymore, so set `items` empty.
//
// This is all up to you and your app though:
// maybe you want to keep the items around!
// Do whatever seems right for your use case.
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
// ALWAYS have a default case in a reducer
return state;
}
}
Finally, we just need to pass the products into a ProductList
component that will display them, and also be responsible for kicking off the data fetching.
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";
class ProductList extends React.Component {
componentDidMount() {
this.props.dispatch(fetchProducts());
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<ul>
{products.map(product =>
<li key={product.id}>{product.name}</li>
)}
</ul>
);
}
}
const mapStateToProps = state => ({
products: state.products.items,
loading: state.products.loading,
error: state.products.error
});
export default connect(mapStateToProps)(ProductList);
I’m referring to the data with state.products.<whatever>
instead of just state.<whatever>
because I’m making the assumption that you’ll probably have more than one reducer, each handling its own slice of state. To make this work, we can write a rootReducer.js
file that pulls them all together:
import { combineReducers } from "redux";
import products from "./productReducer";
export default combineReducers({
products
});
Then, when we create our store, we can pass this “root” reducer:
import rootReducer from './rootReducer';
// ...
const store = createStore(rootReducer);
Error Handling in Redux
The error handling here is pretty light, but the basic structure will be the same for most actions that make API calls. The general idea is:
- Dispatch a FAILURE action when the call fails
- Handle that FAILURE action in the reducer by setting some kind of flag and/or saving the error message.
- Pass the error flag and the message (if you have one) into components that need to handle errors, and condtionally render the error however you see fit.
But It Will Render Twice!
This is a really common concern. And yes, it will render more than once.
It will render in an empty state, then re-render in a loading state, and then re-render again with products to show. The horror! 3 renders! (you could get it down to 2 if you skip straight to the “loading” state)
You may be worried about unnecessary renders because of performance, but don’t be: single renders are very fast. If you’re working on an app where they are slow enough to notice, do some profiling and figure out why that’s the case.
Think of it this way: the app needs to show something when there are no products, or when they’re loading, or when there’s an error. You probably don’t want to just show a blank screen until the data is ready. This gives you an opportunity to make that user experience shine.
But The Component Shouldn’t Have to Fetch!
From an architecture standpoint, it would be nicer if there was a parent “thing” (component or function or router or whatever) that automatically fetched data before it loaded the components. Then components could be blissfully unaware of any dirty API nonsense; they could simply wait around to be handed data on a silver platter. What a life!
There are ways to fix this, but as with everything, they come with tradeoffs. Magic data loaders are magic (harder to debug, harder to remember how/when/why they work). They might require more code instead of less.
Many Ways To Solve Data Fetching
There are many many ways to factor this code. There is no “best way,” because these things exist on a spectrum, and because the “best” for one use case may be the “worst” for another.
“Fetch the data in componentDidMount
” is not the one true way, but it’s simple, and it gets the job done.
If you don’t like the idea of doing it this way, though, here are some other things you could try:
- Move the API call out of the Redux action and into an
api
module, and call it from the action. (better separation of concerns) - Have the component call the API module directly, and then dispatch the action from inside the component when the data comes back, like Dan Abramov shows in this video.
- Use a library like redux-dataloader or redux-async-loader or one of the other libraries from Mark Erikson’s list of data fetching libs.
- Make a wrapper component to do the fetching – in the above example, that might be called
ProductListPage
. Then the “Page” takes care of fetching, and the “List” just accepts data and renders it. - Use recompose to pull out the
componentDidMount
lifecycle into its own higher-order wrapper component – and although the library will keep working, it seems its creator has decided to stop working on it in light of React’s new hooks feature. - Soon (or maybe even now) you’ll be able to use React’s built-in Suspense feature to fetch and cache data.
Like I said, there are a lot of ways to do this :)
Working Code Example
Check out this CodeSandbox to see a working version of this app.
Since there’s no server to handle the request here, I wrote a fakeFetchProducts
function to fake a delay and then return the data. This way you can see how the loading indicator works without having to set up a server.
Action Steps
Try implementing this yourself! Practice is the best way to learn.
If you don’t have your own backend server, just use Reddit – their URLs will return JSON if you append “.json” to the end, e.g. www.reddit.com/r/reactjs.json.
Here’s an exercise for you: Make a tiny React + Redux app that displays the posts from /r/reactjs.