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
How This Works, in Code
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:
productActions.js
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)));
};
}
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
Side note: fetch()
does not throw HTTP errors. This is really confusing if you’re used to something like axios. Read here for more about fetch and error handling.
Redux actions that fetch data will typically be written with begin/success/failure actions surrounding the API call. This isn’t a requirement, but it’s useful because it lets you 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 Error
. Here’s what those actions look like:
productActions.js
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 fetchProductsError = 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.
productReducer.js
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:
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_ERROR:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
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.
ProductList.js
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "productActions";
class ProductList extends React.Component {
render() {
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);
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 easy opportunity to make those decisions.
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, there are 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 Skin this Cat
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 things you could try:
Like I said, there are a lot of ways to do this :)
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.
Make a tiny React + Redux app that displays the posts from /r/reactjs.
Where and When to Fetch Data With Redux was originally published by Dave Ceddia at Dave Ceddia on February 02, 2018.
CodeProject