Redux Thunk

Redux Thunk middleware allows you to write action creators that return a function instead of an action. The Thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters.

What is a middleware?

Middleware is computer software that connects software components or applications. The software consists of a set of services that allows multiple processes running on one or more machines to interact.

The redux-thunk middleware sits in between the dispatch and reducers, which means we can alter our dispatched actions before they get to the reducers or execute some code during the dispatch.

When to use?

When we need to do any kind of asynchronous logic, we generally pair it with axios, which is a library for making HTTP requests. One of my favorite reasons to use axios over ES6 fetch is that with axios you don’t have to do convert the data you obtained from your request using .json(), axios does this automatically, for example:

fetch(url)
  .then((response) => response.json())
  .then((data) => console.log(data));

axios.get(url).then((response) => console.log(response));

Installing dependencies

For an already created project:

yarn add redux react-redux

If you’re doing some async

yarn add axios redux-thunk

Project Structure

src/
├── context
├── actions
├── api
├── components
├── pages
├── reducers
├── store
├── types
├── App.js
└── index.js

Each folder will have an index.js file inside.

Types

Inside the types folder we add our constants.

// Types as constants
export const FETCH_GAMES_REQUESTED = "FETCH_GAMES_REQUESTED";
export const FETCH_GAMES_SUCCESS = "FETCH_GAMES_SUCCESS";
export const FETCH_GAMES_ERROR = "FETCH_GAMES_ERROR";

Actions

Inside the actions folder we add our actions creator, importing the constants we made.

// Action Creators
import {
  FETCH_GAMES_REQUESTED,
  FETCH_GAMES_SUCCESS,
  FETCH_GAMES_ERROR,
} from "../types";

export const fetchGames = () => ({ type: FETCH_GAMES_REQUESTED });
export const fetchGamesSuccess = (payload) => ({
  type: FETCH_GAMES_SUCCESS,
  payload,
});
export const fetchGamesError = (error) => ({ type: FETCH_GAMES_ERROR, error });

Remember, an action creator is merely a function that returns an action object.

API

This is where we will use axios, we will do all our requests on these folders and it will look like this:

// API
export const fetchGame = () => axios.get("http://localhost:3000/games");
export const addGame = (payload) =>
  axios.post("http://localhost:3000/games", { ...payload });
export const editGame = (payload) =>
  axios.delete(`http://localhost:3000/games/${payload.id}`, { ...payload });
export const deleteGame = (payload) =>
  axios.delete(`http://localhost:3000/games/${payload.id}`);

Reducer

This is where our reducer will be, anytime we dispatch an action in our app, the reducer receives the command to change the global state.

import {
	FETCH_GAMES_LOADING,
   	FETCH_GAMES_SUCCESS,
    FETCH_GAMES_ERROR,
} from '../types'

// Initial State
const initialState = {
    data: [],
    loading: false,
    error: ""
}

// Reducer
const rootReducer = (state = initialState, action) => {
    switch(action.type) {
        case FETCH_GAMES_LOADING:
            return {
                ...state,
                loading: true
            }
         case FETCH_GAMES_SUCCESS:
            return {
                ...state,
                data: action.data,
                loading: false
            }
         case FETCH_GAMES_LOADING:
            return {
                ...state,
                error: action.error
                loading: false
            }
    }
}

Notice how it only does something when we do only a fetch request, it’s because our POST, DELETE and PUT will only do a HTTP request and won’t mess with the global state, but when we do a GET the app will reload.

Store

This is where our store will be configured/

import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from '../reducers'

export function configureStore(initialState) {
    const middleware = [thunk]
    const composeEnhancers = window.__REDUX_DEVTOOLS, EXTENSION_COMPOSE__ || compose
    const store = createStore(rootReducer, initialState, composeEnchancers(
    	applyMiddleware(...middleware)
    ))
    return store
}

The store takes as argument the rootReducer, the initial state and with composeEnhancers we can add several middleware’s like Redux Thunk.

Connect store to provider

We configure our store importing the setup from store folder.

import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { configureStore } from "./store";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

const store = configureStore();

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();

This way we can “provide” the store for the whole application.

Ducks pattern

Another way of making this process even easier, is to put everything on a single file, but of course I don’t mean EVERYTHING, but if in your app you’re working with getting a set of posts or comments in a blog, then you can do two ducks files. In the case of this tutorial we were only doing it for a set of games, which we will see in detail in another post, but for now lets see how a ducks pattern would look here:

import axios from "axios";
import thunk from "redux-thunk";
import { createStore, applyMiddleware, compose } from "redux";

// Types as constants
export const FETCH_GAMES_REQUESTED = "FETCH_GAMES_REQUESTED";
export const FETCH_GAMES_SUCCESS = "FETCH_GAMES_SUCCESS";
export const FETCH_GAMES_ERROR = "FETCH_GAMES_ERROR";

// Actions
export const fetchGames = () => ({ type: FETCH_GAMES_REQUESTED });
export const fetchGamesSuccess = (payload) => ({
  type: FETCH_GAMES_SUCCESS,
  payload,
});
export const fetchGamesError = (error) => ({ type: FETCH_GAMES_ERROR, error });

// API
const URL = "http://localhost:3000/games";
export const addGame = (payload) => () => axios.post(URL, { ...payload });
export const deleteGame = (payload) => () =>
  axios.delete(`${URL}/${payload.id}`);
export const editGame = (payload) => () =>
  axios.put(`${URL}/${payload.id}`, { ...payload });
export const fetchGames = () => (dispatch) => {
  dispatch({ type: FETCH_GAMES_LOADING });
  axios.get(URL).then(
    (data) => dispatch({ type: FETCH_GAMES_SUCCESS, data }),
    (error) =>
      dispatch({ type: FETCH_GAMES_ERROR, error: error.message || "ERROR" })
  );
};

// Intial State
const initialState = {
  data: [],
  loading: false,
  error: "",
};

// Root Reducer
const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_GAMES_LOADING: {
      return {
        ...state,
        loading: true,
      };
    }
    case FETCH_GAMES_SUCCESS: {
      return {
        ...state,
        data: action.data,
        loading: false,
      };
    }
    case FETCH_GAMES_ERROR: {
      return {
        ...state,
        loading: false,
        error: action.error,
      };
    }
    default: {
      return state;
    }
  }
};

export const configureStore = (initialState) => {
  const middleware = [thunk];
  const composeEnhancers =
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
  const store = createStore(
    rootReducer,
    initialState,
    composeEnhancers(applyMiddleware(...middleware))
  );
  return store;
};

As you can see this pattern makes everything more smooth and it’s recomended in the Redux documentation.

Summary

Conclusion

We learned how to setup a Redux structure in our React app, it’s very straightforward when you start doing it, but there are even easier ways of doing this kind of setup, with a ducks pattern (everything on a single file) as we just saw and using the new Redux Toolkit which we will see later.

See you on the next post.

Sincerely,

Eng. Adrian Beria.