DEV Community 👩‍💻👨‍💻

delph
delph

Posted on

handling asynchronous actions with redux-thunk

* this article assumes some basic knowledge in redux

Redux

Redux is a library commonly used for managing global state in React applications. Redux works great for state updates for synchronous actions (eg. incrementing/ decrementing a counter), but more often that not, most applications will need to perform some sort of asynchronous action (eg. making an API call to fetch data from the server).

redux-thunk

redux-thunk is a middleware that allows you to write asynchronous logic that interacts with the store. A redux middleware, as the name suggests, sits in the middle between the moment an action is dispatched, and the moment it reaches the reducer.

getting started

first, create your react app and install dependencies

npm install redux react-redux redux-thunk axios --save

or

yarn add redux react-redux redux-thunk axios

index.js

In your root index.js file, import the Provider from 'react-redux' as per normal and wrap the App component with it so that the entire app has access to the redux store.

We will also need to import createStore from 'redux' as per normal. The only difference is that we also need to import applyMiddleware, a function from 'redux', and thunk from 'redux-thunk'. This will be passed in as the second argument when we create the store, so that whenever we dispatch an action, the action will be first sent to redux thunk as the middleware.

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

import App from './components/App';
import reducers from './reducers';

const store = createStore(reducers, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#root')
);

At this point, we will get some errors since we have not created our 'App' component and reducers. but first, let's do some configuration and create some action creators to fetch our data.

API configuration

While this step is not necessary, is is useful to create an axios instance and specify a base URL in an apis folder. By pre-configuring axios, we do not need to specify the base each time we make a request.

For this example, we will be fetching a list of posts from jsonplaceholder.

// src/apis/jsonPlaceholder.js
import axios from 'axios';

export default axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com'
})

action creators

the main difference between normal synchronous applications and asynchronous actions with redux thunk lies in this step.

generally, for redux, an action creator is simply a function that returns a plain javascript object with a type property (and occasionally some other properties such as 'payload' etc.)

with redux thunk, an action creator can also optionally return a function instead of an action object. our action creator to fetch posts would then look something like this:

// src/actions/index.js
import jsonPlaceholder from '../apis/jsonPlaceholder';

// normal action creator
export const clearPosts = () => ({
    type: 'CLEAR_POSTS'
})

// thunk action creators
export const fetchPosts = () =>  async dispatch => {
  const response = await jsonPlaceholder.get('/posts')

  dispatch({type: 'FETCH_POSTS', payload: response.data})
 }

export const fetchUser = id =>  async dispatch => {
  const response = await jsonPlaceholder.get(`/users/${id}`)

  dispatch({type: 'FETCH_USER', payload: response.data})
 }


in addition to the dispatch argument, we can optionally pass in a second argument, getState, which would give us total control over changing or getting information out of our redux store.

// src/actions/index.js

export const fetchPostsAndUsers = id =>  async (dispatch, getState) => {
  await dispatch(fetchPosts())

  const userIds = _.uniq(_.map(getState().posts, 'userId'))

  userIds.forEach(id => dispatch(fetchUser(id)))
 }


reducers

nothing too different here.

// src/reducers/index.js
import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import userReducer from './userReducer';

export default combineReducers({
  posts: postsReducer,
  users: userReducer
});

// src/reducers/postsReducer.js
export default (state = [], action) => {
  switch (action.type) {
    case 'FETCH_POSTS':
      return action.payload;
    default:
      return state;
  }
};

// src/reducers/userReducer.js
export default (state = [], action) => {
  switch (action.type) {
    case 'FETCH_USER':
      return [...state, action.payload];
    default:
      return state;
  }
};

finally, our App.js

as per normal redux, we need to import connect from 'react-redux' in order to access the state in our redux store.

// src/components/App.js

import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';

class App extends React.Component {
  componentDidMount() {
    this.props.fetchPosts();
  }

  renderList() {
    return this.props.posts.map(post => {
      return (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      );
    });
  }

  render() {
    return <div>{this.renderList()}</div>;
  }
}

const mapStateToProps = state => {
  return { posts: state.posts };
};

export default connect(
  mapStateToProps,
  { fetchPosts }
)(App);

Top comments (0)

We are hiring! Do you want to be our Senior Platform Engineer? We're hiring for a Senior Platform Engineer and would love for you to apply.

Head here to learn more about who we're looking for.