React – Redux Thunk – Load, Create & Delete Operations

Overview

This article will attempt to walk through of how to use redux with redux-thunk with a simple application to load, add, and delete records. Basically, the asynchronous action creators will handle the API calls and the reducer will modify the state of the list. The API is built with ASP.NET C# Web API and uses a local MDF file. This post will not go over the API or how to build the components because I wanted to focus on the state management logic, The entire application (UI & API) is available from Github.

Use Case for Redux Middleware

With a Redux store, you can only run synchronous actions. Middleware extends the store’s abilities to run asynchronous actions such as API calls.

Redux middleware can also keep your components lean by centralizing API operations and handling the state in memory. Using this pattern helps manage large or complex data operations into smaller and readable files. Managing state in memory can also cut down number of API calls since you don’t have to read from the database after a CRUD operation. Storing state in memory improves the user experience since the store can keep a recorded history of the data operations such as the search results.

Source Files for This Article

Source Code

Prerequisites

Please install the following libraries.

Redux

npm install --save redux
npm install --save react-redux

Axios

Axios is a promise-based HTTP Client.

npm i axios

Middleware

npm install --save redux-thunk  

Dev Tool (Optional)

Install the dev tool so you can watch the state changed in a browsers dev tool with the Redux plugin.

npm install --save-dev redux-devtools  

Create the Store in a Separate File

In the index.js file import the “createStore” and “Provider“. Wrap the root <App/> component with Provider so the entire application has access to the store. Below are a couple of different approaches to adding a store.

index.js

Create a file is labeled “index.js“. The store is set up to handle multiple reducers and slices.

import { configureStore } from '@reduxjs/toolkit';
import { combineReducers, createStore, applyMiddleware } from 'redux';
import userReducer from './userReducer';
import productReducer from './productReducer';

import thunk  from 'redux-thunk';

var reducers = combineReducers({userReducer, productReducer, counterReducer});
const store = configureStore({  
  reducer: reducers,      
});
// const store = createStore(reducers, applyMiddleware(thunk));

export default store;

The provider tag is used as a wrapper for <App />. This makes the store accessible to all of the components under the App component.

App.js imports the store from “store/index.js“.

import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store/index';
import './index.css';
import App from './App';
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Actions, Reducers, & Thunk

Under the “/store” folder, create the following files labeled “product-actions.js”, “product-reducers.js“, “product-thunk.js” .

Actions

Below is the source code for the “product-actions.js” file. These are the actions that are dispatched from the return function in the action creators in the product-thunk file. The actions are only defined for successful API calls to keep this example short.

export const PRODUCTS_GET_SINGLE_RECORD_SUCCESS = '[PRODUCTS] GET SINGLE RECORD SUCCESS';
export const PRODUCTS_LOAD_RECORDS_SUCCESS = '[PRODUCTS] LOAD RECORDS SUCCESS';
export const PRODUCTS_ADD_RECORD_SUCCESS = '[PRODUCTS] ADD RECORD SUCCESS';
export const PRODUCTS_DELETE_RECORD_SUCCESS = '[PRODUCTS] DELETE RECORD SUCCESS';

export const loadProductsSuccess = (data) => ({
    type: PRODUCTS_LOAD_RECORDS_SUCCESS,
    payload: data,
});

export const getProductSuccess = (record) => ({
    type: PRODUCTS_GET_SINGLE_RECORD_SUCCESS,
    payload: record,
});

export const addProductSuccess = (record) => ({
  type: PRODUCTS_ADD_RECORD_SUCCESS,
  payload: record
});
  
export const deleteProductSuccess = (id) => ({
    type: PRODUCTS_DELETE_RECORD_SUCCESS,    
    payload: id
});

Thunk

Action creators return a function instead of an action object. These functions can have side effects like an asynchronous API call and dispatch actions. Below are the action creators in the “product-thunk.js“.

import axiosConn from '../global/axiosConn';
import { loadProductsSuccess, 
         getProductSuccess,          
         deleteProductSuccess, 
         addProductSuccess} from './product-actions';

export const loadProducts = () => {
  return async function (dispatch) {
    const url = `/api/product/getall`;
    
    await axiosConn      
      .get(url)
      .then((resp) => {
        console.log("resp", resp);
        dispatch(loadProductsSuccess(resp.data.data));
      })
      .catch((error) => console.log(error));
  };
};

export const getProduct = (id) => {
  return async function (dispatch) {
    const url = `/api/product/getsinglerecord/${id}`;

    await axiosConn      
      .get(url)
      .then((resp) => {
        console.log("resp", resp);
        dispatch(getProductSuccess(resp.data.data));
      })
      .catch((error) => console.log(error));
  };
};


export const deleteProduct = (id) => {
  return async function (dispatch) {
    const url = `api/product/remove/${id}`;      

    await axiosConn      
      .delete(url)
      .then((resp) => {          
        dispatch(deleteProductSuccess(id));        
      })
      .catch((error) => console.log(error));
  };
};

export const addProduct = (record) => {
  return async function (dispatch) {    
    const url = `api/product/add`;

    await axiosConn      
      .post(url, {
          Title: record.Title,
          Description: record.Description,          
          Quantity: record.Quantity
      })
      .then((resp) => {        
        dispatch(addProductSuccess(resp.data.data));        
      })
      .catch((error) => console.log(error));
  };
};

Reducers

Reducers are pure functions that takes an action and the previous state of the application and returns the new state, Being a pure function if the same input is passed to it the result is always same.

Under the store folder create a reducer file labeled “product-reducers.js“.

In the reducer file import the actions file and add the following switch statement:

import * as actionTypes from './product-actions';

const initialState = {    
    products: [],    
    productRecord: [],    
    loading: true
};

const productReducers = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.PRODUCTS_LOAD_RECORDS_SUCCESS: {
      return {
        ...state,
        products: action.payload,
        loading: false,
      };    
    }
  
    case actionTypes.PRODUCTS_DELETE_RECORD_SUCCESS: {
      return {
        ...state,        
        //* Get all the records exceot the one that was deleted
        products: state.products.filter(products => products.Id !== action.payload), 
        loading: false      
      }
    }

    case actionTypes.PRODUCTS_ADD_RECORD_SUCCESS: {
      const newProductsArray = [...state.products];      
      newProductsArray.splice(2, 0, action.payload);      
      return {
        ...state,        
        products: newProductsArray,
        loading: false
      };
    }

    case actionTypes.PRODUCTS_GET_SINGLE_RECORD_SUCCESS: {
      return {
        ...state,
        product: action.payload,
        loading: false,
      };
    }

    default:
      return state;
  }
};

export default productReducers;

Breakdown of a Single Action Creator

Add Product Action

Below is the work flow for adding a record.

The “AddFormDialog” component dispatches “AddProduct” instead of making the API call and then dispatching the action in the response of the API.

The “handleSubmit” method in “AddFormComponent will dispatch the “addProduct” thus keeping the component lean.

import { useDispatch } from 'react-redux';
import { addProduct  } from '../../store/product-actions';
...

const handleSubmit = e => {
  const newData = {Title: title, Description: description};
  dispatch(addProduct(newData))    
  setOpen(false);
}

The action creator in the “product-thunk.js” file.

export const addProduct = (record) => {
  return async function (dispatch) {    
    const url = `api/product/add`;

    await axiosConn      
      .post(url, {
          Title: record.Title,
          Description: record.Description          
      })
      .then((resp) => {        
        dispatch(addProductAction(resp.data.data));        
      })
      .catch((error) => console.log(error));
  };
};

Then the action creator above makes an asynchronous call to the API and then returns a function that dispatches the PRODUCTS_ADD_RECORD action in “product-actions.js“.

const addProductAction = (record) => ({
  type: actionType.PRODUCTS_ADD_RECORD,
  payload: record
});

Finally through “addProductAction = (record)” dispatches the action PRODUCTS_UPDATE_RECORD in “product-reducers.js“.

case actionTypes.PRODUCTS_ADD_RECORD_SUCCESS: {
   const newProductsArray = [...state.products];      
   newProductsArray.splice(2, 0, action.payload);      
   return {
    ...state,        
    products: newProductsArray,
    loading: false
  };
}

Get Data From Store

Use the useSelector() hook to get data from the store.

The data available in this example is defined in the Reducer – “.\store\product-reducer.js”

const initialState = {    
    products: [],    
    productRecord: [],    
    loading: true
};

By looking at the output of the state. you can see all the variables and arrays.

How to view the Reducer State Variables

import { useSelector } from "react-redux";
...

useSelector((state) => console.log('SHOW STATE', state));

Example: In the Component import useSelector() and get the Products data as follows:

import { useSelector } from "react-redux";
...

const productData = useSelector((state) => state.productReducers.products);

OR

It could be prefixed with state.reducers.REDUCER_NAME.variable.

import { useSelector } from "react-redux";
...

const productData = useSelector((state) => state.reducers.productReducers.products);

Source Files for This Article

Source Code