Global state management using redux and redux-thunk

Author : JaNakh Pon , January 04, 2022

Tags

Summary

In this article, we are going to integrate redux and redux-thunk for our Todo app's functionalities(fetch, search, sort, create, update, delete ) in React.js.

We will use redux for complex state management, however, since redux doesn't support impure functions out of the box, we will need to use redux-thunk as a middleware to handle async/impure functions.


Installation & Setup

Firstly, let's install the required dependencies 😉:

  > npm i react-redux redux-thunk --save

Now, let's a folder named 'redux' under src/* and create actions, reducers, store and types folders under the redux/* folder.

Then we'll start by creating store, we will need to pass our reducers to the Redux createStore function, which returns a store object.

redux/store/index.js
import rootReducer from "../reducers";
import {createStore,applyMiddleware} from "redux";
import thunk from "redux-thunk";

const store = createStore(rootReducer,applyMiddleware(thunk));
export default store;

Note that we can also use other middlewares alongside thunk if we need. For example:

redux/store/index.js
const store = createStore(rootReducer,applyMiddleware(thunk),applyMiddleware(othermiddleware));

Now, in our root index.js, let's import store object and then pass this object to the react-redux Provider component, which is rendered at the top of our component tree.

This ensures that any time we connect to Redux in our app via react-redux connect, the store is available to our components.

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
reportWebVitals();

types, actions and reducers

Types

Let's start by defining types values:

redux/types/task.js
export const TASK_COUNT = 'TASK_COUNT'
export const TASK_ERROR = 'TASK_ERROR'
export const ADD_TASK = 'ADD_TASK'
export const TASK_UPDATE = 'TASK_UPDATE'
export const TASK_REMOVE = 'TASK_REMOVE'
export const TASK_LIST = 'TASK_LIST'
export const TASK_SEARCH = 'TASK_SEARCH'
export const URL = `http://localhost:3001/api/v1/todos`

Actions

Actions are a plain JavaScript object that contains information. Actions are the only source of information for the store. Actions have a type field that tells what kind of action to perform and all other fields contain information or data. And there is one other term called Action Creators, these are the function that creates actions. So actions are the information (Objects) and action creator are functions that return these actions.

Let's start by creating action creators for create/read/update/delete/sort/filter/search functionalities:

redux/actions/task/index.js
import axios from "axios";
import {
    URL,
    TASK_COUNT,
    TASK_LIST,
    TASK_ERROR,
    TASK_REMOVE,
    TASK_UPDATE,
    TASK_SEARCH,
} from "../../types/task";

export const getTasksCount = () => async (dispatch) => {
    let tkCount = await axios({
        method: "get",
        url: `${URL}/count`,
    });
    if (tkCount.data.err !== "") {
        dispatch({
            type: TASK_ERROR,
            payload: tkCount.data.err,
        });
    }
    if (tkCount.data.count) {
        dispatch({
            type: TASK_COUNT,
            payload: tkCount.data.count,
        });
    }
};

export const listTasks = (page, sort, order) => async (dispatch) => {
    let tkList = await axios({
        method: "get",
        url: `${URL}?page=${page}&sort=${sort}&order=${order}`,
    });
    if (tkList.data.err !== "") {
        dispatch({
            type: TASK_ERROR,
            payload: tkList.data.err,
        });
    }
    if (tkList.data) {
        dispatch({
            type: TASK_LIST,
            payload: tkList.data,
        });
    }
};

export const removeTask = (id) => async (dispatch) => {
    await axios({
        method: "delete",
        url: `${URL}/${id}`,
    });
    dispatch({
        type: TASK_REMOVE,
        payload: id,
    });
};

export const updateTask = (completed, id) => async (dispatch) => {
    let tkResp = await axios({
        method: "put",
        url: `${URL}/${id}`,
        data: { completed: completed }
    });
    dispatch({
        type: TASK_UPDATE,
        payload: tkResp.data,
    });
};

export const searchTasks = (text) => async (dispatch) => {
    let tkResp = await axios({
        method: "get",
        url: `${URL}/search?text=${text}`
    });
    dispatch({
        type: TASK_SEARCH,
        payload: tkResp.data,
    });
};

Reducers

Actions only tell what to do, but they don't tell how to do, so reducers are the pure functions that take the current state and action and return the new state and tell the store how to do.

Basically, reducer follows the "message" from action.type and define what to do with data from action.payload.

redux/reducers/task/index.js
import * as actionTypes from "../../types/task";
const initialState = {
  taskList: [],
  searchList: [],
  task: {},
  count: 0,
  searchCount: 0,
  error: null,
  msg: null,
  loading: false,
};

export const task = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.TASK_COUNT:
      return {
        ...state,
        count: action.payload,
      };
    case actionTypes.TASK_LIST:
      return {
        ...state,
        taskList: action.payload,
      };
    case actionTypes.TASK_REMOVE:
      return {
        ...state,
        count: parseInt(state.count - 1),
        taskList: state.taskList.filter(tk => tk.id !== action.payload),
      };
    case actionTypes.TASK_UPDATE:
      return {
        ...state,
        taskList: state.taskList.map(x => (x.id === action.payload.id ? { ...x, completed: action.payload.completed } : x))
      };
    case actionTypes.TASK_SEARCH:
      return {
        ...state,
        searchCount: action.payload.count,
        searchList: action.payload.data,
      };
    default:
      return state;
  }
};

Implementation

In this step, we will use useSelector to get state data and useDispatch to change the state in our components:

App.js
import { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import * as actions from "./redux/actions";

const App = () => {
  const { task } = useSelector((obj) => obj);
  const dispatch = useDispatch();
  .
  .
  .

  useEffect(() => {
    dispatch(actions.getTasksCount());
  }, [newtask, dispatch]);

  useEffect(() => {
    dispatch(actions.listTasks(page, sort, order));
  }, [newtask, page, sort, order, dispatch]);
  .
  .

  return (
    <div className="container mx-auto lg:my-32 md:my-30 sm:my-15 ">
     .
     .
    </div>
  );
};

export default App;

If you are using class components instead of functional components, check this old repos with HOC: redux and redux with redux-thunk.

Ref => Rules of Reducers

Ref => Pure vs Impure Functions in Functional Programming

Ref => Pure vs Impure Functions

Source Code.