Handling asynchronous fetching of data with Redux

This article is a quick explanation and guide on how to use redux with async functions. Most common case is fetching data from remote source (eg. an API on the internet).

A full working example can be found here:
https://snack.expo.io/@killerchip/redux-async

The example is build with React-Native, but the core concepts are exactly the same for web.

What we know so far

Up to now you should be familiar with the classic redux model. In short:

  • A state is stored centrally in a store.
  • The UI subscribes to state via the connect HOC from react-redux. It renders and re-renders, following changes in the state.
  • State changes are launched with the help of action-creators. An action creator function actually return an action-object, which has at least the type property.
  • The action-object is passed to redux‘s dispatch function, which eventually passes it to the reducer function, we define.
  • The reducer function composes and returns a new state, based on the received action-object.
  • The returned state is becoming new state in the store.
  • And the changes are propagated to UI parts that have subscribed to it.
  • And the story goes on…

Handling Asychronous operations with synchronous actions

Let’s keep it simple. Imagine we have a simple app, fetching popular cat names from the internet. The app typically:

  • Display a list of fetched cat names.
  • Display a message, icon, or in general a component when it is actually fetching data from the internet.
  • If the cat names are fetched OK, then we display them.
  • If the fetch operation failed for some reason, display the error message.

So, for a simple list of cat names, we probably need the following state form:

1
2
3
4
5
const initialState = {
data: null,
isFetching: false,
error: null
}
  • data holds the latest cat names fetched from the internet
  • isFetching is indicating whether a fetch operation is in progress.
  • error holds the error object from our last fetch operation, if it failed. If not, then error is null.

The above state should be enough to allow our UI display the information as requested above.

But launching a single fetch operation requires many changes in our state. Additonally these changes are occuring asynchronously.
Let’s follow the chain of events…

A fetch operation is lauched: The state updates to:

1
2
3
{
isFetching: true
}

Case 1: The fetch operation succeeds and returns newData:

1
2
3
4
5
{
data: newData,
isFetching: false,
error: null
}

Case 2: The fetch operation fails with an error object:

1
2
3
4
5
{
data: null, // or we may choose to keep the old data from previoius fetch.
isFetching: false,
error: error
}

So we need the corresponding synchronous action-objects and their action-creators:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const CAT_NAMES = {
START_FETCH: "CAT_NAMES_START_FETCH",
FAIL_FETCH: "CAT_NAMES_FAIL_FETCH",
FINISH_FETCH: "CAT_NAME_FINISH_FETCH"
};

export const startFetchCatNames = () => ({
type: CAT_NAMES.START_FETCH
});

export const failFetchCatNames = error => ({
type: CAT_NAMES.FAIL_FETCH,
payload: error
});

export const finishFetchCatNames = data => ({
type: CAT_NAMES.FINISH_FETCH,
payload: data
});

And a reducer function that changes the state accordingly:

1
2
3
4
5
6
7
8
9
10
11
12
const reducer = (state = initialState, action) => {
switch (action.type) {
case CAT_NAMES.START_FETCH:
return { ...state, isFetching: true };
case CAT_NAMES.FINISH_FETCH:
return { isFetching: false, data: action.payload, error: null };
case CAT_NAMES.FAIL_FETCH:
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}
};

Now each time we wish to launch a new fetch action, we actually have to perform the asynchronous logic in a separate function that dispatches separate actions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import store from './store.js' // or whatever module exports your store

function fetchCatNames = async url => {
const {dispatch} = store;

dispatch(startFetchCatNames()); // indicate that fetch has started

try {
const data = await fetchData(url); // fetching data
dispatch(finishFetchCatNames(data)); // handle success
} catch (error) {
dispatch(failFetchCatNames(error)); // handle failure
}
}

Well this is a bit out of our react-redux pattern. We access the store directly. And can get things a bit more complicated. Wouldn’t be nice if our function could be return by an action-creator and be injected in our redux chain of action-processing seamlessly with the other good-old, action objects?

redux-thunk middleware

Well, a more elegant approach to this, is the redux-thunk middleware.!

Wow! A lot of terms. Let’s explain:

Middleware: redux allows for middleware plugins. This means that each action that is dispatched, before is delivered to redux‘s reducer, can be handed-over to 3rd party tools. These can modify actions, tigger side actions, or even prevent redux actions from executing.

redux-thunk: This is a specific plugin that can handle asynchronous functions.
Actually redux-thunk will receive an action before it is handed-over to reducer. It will check the type of the action. If the action is a function, then it will execute it, and stop further proecessing. If it is an object, then it will pass it on untouched, and be done with it.

So we can pass our asynchronous function as an action, and have it launched automatically by redux-thunk. The actual function will then trigger a series of synchronous events based on the progress of our fetch operation.

Creating a thunk

A thunk actually is a function that is returned from another function. So it defers execution for later.

A redux-thunk is a function that accepts dispatch as a parameter and uses it as it wishes to dispatch actual actions.
So in our case, our fetchCatNames function as thunk should be:

1
2
3
function fetchCatNames = dispatch => {
// our rest of logic does not change...
}

But what if we want to pass our own parameters to our thunk? Then we create our function (a thunk) via an action creator. So our special action-creator returns a function and not a redux action-object.

1
2
3
4
5
6
7
8
9
10
export const fetchCatNames = url => async dispatch => { // <== notice the two fat-arrows
dispatch(startFetchCatNames());

try {
const data = await fetchData(url);
dispatch(finishFetchCatNames(data));
} catch (error) {
dispatch(failFetchCatNames(error));
}
};

Actually we need to create our thunk via an action-creator also because it will be used by react-redux exactly in the same way as our typical synchronous action-creators. It’s just that it returns a function (thunk) instead of an object, and it will be handled by redux-thunk middleware.

So when one or more components, need to call our asynchronous action, we pass its action creator the same way as we would with the our classic action-creators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ButtonBar extends React.PureComponent {
fetchCatName = () => this.props.fetchCatNames('data'); // <== calling action creator is equivalent to passing it to dispatch. See below

render() {
return (
<View style={styles.view}>
<Button onPress={this.fetchCatName}>Fetch Names</Button>
</View>
);
}
}

const mapActionsToProps = {
fetchCatNames, // <== we pass our action creator to the component, here
};

export default connect(
null,
mapActionsToProps
)(ButtonBar);

Note: In the above example we used the mapActionsToProps object. This is a form of react-redux that allows us directly to call our _action_creators_. In the background our action creators will be passed to the dispatch function as argument. More details here.

Note: we said that our thunk receives dispatch as parameter. Well, it actually can also receive getState function, in case it needs the current state in its logic. See more details here.

Applying the middleware

This is an easy step. We just apply the middleware using applyMiddleware function when we define our store.

1
2
3
4
5
6
7
8
9
10
11
12
import { combineReducers, applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import catNames from "./reducer";

const reducer = combineReducers({
catNames
});

export default createStore(
reducer,
applyMiddleware(thunk) // <== Applying redux-thunk middleware
);

Summary / Cheatsheet:

So in order to handle asynchronous operations with redux we need to define special functions that return other functions (thunks).

Step 1: we define the actions and state as normal. They are synchronous actions:

actions.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const CAT_NAMES = {
START_FETCH: "CAT_NAMES_START_FETCH",
FAIL_FETCH: "CAT_NAMES_FAIL_FETCH",
FINISH_FETCH: "CAT_NAME_FINISH_FETCH"
};

export const startFetchCatNames = () => ({
type: CAT_NAMES.START_FETCH
});

export const failFetchCatNames = error => ({
type: CAT_NAMES.FAIL_FETCH,
payload: error
});

export const finishFetchCatNames = data => ({
type: CAT_NAMES.FINISH_FETCH,
payload: data
});

reduxer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { CAT_NAMES } from "./actions";

const initialState = {
data: null,
isFetching: false,
error: null
};

const reducer = (state = initialState, action) => {
switch (action.type) {
case CAT_NAMES.START_FETCH:
return { ...state, isFetching: true };
case CAT_NAMES.FINISH_FETCH:
return { isFetching: false, data: action.payload, error: null };
case CAT_NAMES.FAIL_FETCH:
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}
};

export default reducer;

Step 2: we define our asynchronous action creator that handles the asynchronous operations.

actions.js

1
2
3
4
5
6
7
8
9
10
11
12
import { fetchData } from '../api/client';

export const fetchCatNames = url => async dispatch => {
dispatch(startFetchCatNames());

try {
const data = await fetchData(url);
dispatch(finishFetchCatNames(data));
} catch (error) {
dispatch(failFetchCatNames(error));
}
};

Step 3: When defining the store, we apply the redux-thunk middleware.

store.js

1
2
3
4
5
6
7
8
9
import { combineReducers, applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import catNames from "./reducer";

const reducer = combineReducers({
catNames
});

export default createStore(reducer, applyMiddleware(thunk));

Step 4: we bind our action creator normally via the connect HOC of react-redux.

ButtonBar.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Button } from 'react-native-paper';
import { connect } from 'react-redux';
import { fetchCatNames } from '../redux/actions';

class ButtonBar extends React.PureComponent {
fetchCatName = () => this.props.fetchCatNames('data');

render() {
return (
<View style={styles.view}>
<Button onPress={this.fetchCatName}>Fetch Names</Button>
</View>
);
}
}

const mapActionsToProps = {
fetchCatNames, // <== action-creator as prop function
};

export default connect(
null,
mapActionsToProps
)(ButtonBar);