Redux Thunk

Syed Aquib AliSyed Aquib Ali
7 min read

Redux Thunk is a middleware that allows you to write action creators that return a function instead of an action. This is particularly useful for handling asynchronous operations, such as API calls. With Redux Thunk, you can dispatch multiple actions from a single action creator, allowing for complex logic and side effects in your Redux actions.

Setting up Redux and Thunk

  1. Install ReactJS
npx create-react-app redux-thunk
  1. install Redux and some more dependencies
npm install @reduxjs/toolkit react-redux axios antd

As it was shown in the previous article Redux, we will follow the similar setup but with Redux thunk middleware.

  1. Create Slice
// ProductSlice.js

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';

// Fetching data from a random API using `createAsyncThunk` function given by redux.
export const fetchData = createAsyncThunk('product/fetch', async () => {
    const { data } = await axios.get('https://fakestoreapi.com/products');
    // You can console the data to see what data's are coming.
    return data;
});

const ProductSlice = createSlice({
    name: 'ProductSlice',
    initialState: {
        products: [],
        status: 'idle', // There will be 4 status: idle, loading, success, failed.
        error: null,
    },
    extraReducers: (builder) => {
        builder
            .addCase(fetchData.pending, (state, action) => {
                state.status = 'loading';
            })
            .addCase(fetchData.fulfilled, (state, action) => {
                state.products = action.payload;
                state.status = 'success';
            })
            .addCase(fetchData.rejected, (state, action) => {
                state.status = 'failed';
                state.error = action.error.message;
            });
    },
});

export default ProductSlice.reducer;

export const { loadProducts } = ProductSlice.actions;
// CartSlice.js

import { createSlice } from '@reduxjs/toolkit';

const CartSlice = createSlice({
    name: 'CartSlice',
    initialState: {
        cart: [],
    },
    reducers: {
        addToCart: (state, action) => {
            const cartItem = state.cart.find(
                (item) => item.id === action.payload
            );
            if (cartItem) {
                cartItem.quantity += 1;
                return;
            }
            state.cart.push({
                quantity: 1,
                id: action.payload,
            });
        },
        removeFromCart: (state, action) => {
            const cartItem = state.cart.find(
                (item) => item.id === action.payload
            );
            if (cartItem) {
                if (cartItem.quantity > 0) {
                    cartItem.quantity -= 1;
                }
                if (cartItem === 0) {
                    state.cart = state.cart.filter(
                        (item) => item.id !== action.payload
                    );
                }
            }
        },
    },
});

export default CartSlice.reducer;

export const { addToCart, removeFromCart } = CartSlice.actions;
  1. Setup the store
import { configureStore } from '@reduxjs/toolkit';
import ProductReducer from './slices/ProductSlice';
import CartReducer from './slices/CartSlice';

export default configureStore({
    reducer: {
        ProductReducer,
        CartReducer,
    },
});
  1. Create components
  • Navigation bar:
// NavBar.js

import React from 'react';
import { CiShoppingCart } from 'react-icons/ci';
import './NavBar.css';
import { useSelector } from 'react-redux';

function NavBar() {

    const cart = useSelector(state => state.CartReducer.cart);

    let count = 0;
    cart.forEach(item => count += item.quantity);

    return (
        <nav>
            <h2 className="left">My Carts</h2>
            <div className="right">
                <div className="cart-layout">
                    <CiShoppingCart />
                    <h3>{count}</h3>
                </div>
            </div>
        </nav>
    );
}

export default NavBar;
/* NavBar.css */

:root {
    --aqua: #7fbfd4;
}

nav {
    width: 100%;
    background-color: #007a7a;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 20px;
    box-shadow: 0px 0px 10px black;
    color: var(--aqua);
    margin-bottom: 10px;
}

.right {
    width: 60%;
    display: flex;
    justify-content: end;
    margin-left: 10px;
}

.cart-layout {
    display: flex;
    gap: 10px;
    align-items: center;
    font-size: 1.5rem;
}
  • Product component:
// Product.js

import React from 'react';
import './Product.css';
import { useDispatch, useSelector } from 'react-redux';
import { addToCart, removeFromCart } from '../../redux/slices/CartSlice';

function Product({ product }) {
    const dispatch = useDispatch();

    const cart = useSelector(state => state.CartReducer.cart);
    const curItem = cart.find(item => item.id === product.id);
    const curQuantity = curItem ? curItem.quantity : 0;

    return (
        <div className="product">
            <img
                className="productImg"
                src={product.image}
                alt={product.title}
            />

            <div className="productInfo">
                <h2 className="productTitle">{product.title}</h2>
                <p className="productPrice">{product.price}$</p>
            </div>
            <div className="cartInfo">
                <button
                    className="btn"
                    onClick={() => dispatch(addToCart(product.id))}
                >
                    +
                </button>
                <h4>{curQuantity}</h4>
                <button
                    className="btn"
                    onClick={() => dispatch(removeFromCart(product.id))}
                >
                    -
                </button>
            </div>
        </div>
    );
}

export default Product;
/* Product.css */

:root {
    --aqua: #7ebed9;
}

.product {
    border: 1px solid #dadada;
    border-radius: 10px;
    margin-bottom: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding-bottom: 10px;
}

.productImg {
    width: 30%;
    margin-top: 10px;
}

.productInfo {
    padding: 10px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.productTitle{
    font-size: 0.5rem;
    padding-bottom: 10px;
}

.productPrice {
    font-size: 0.5rem;
}

.cartInfo {
    width: 70%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-block: 8px;
}

.btn {
    /* padding: 2px; */
    width: 5vw;
    /* height: vh; */
    background-color: var(--aqua);
    border: none;
    color: #d5d5d5;
    cursor: pointer;
    font-size: 1.3rem;
}
  • Product List component:
// ProductList.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from '../../redux/slices/ProductSlice';
import Product from '../Product/Product';
import './ProductList.css';
import { Spin } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';

function ProductList() {
    const dispatch = useDispatch();
    const products = useSelector((state) => state.ProductReducer.products);
    const status = useSelector((state) => state.ProductReducer.status);
    const error = useSelector((state) => state.ProductReducer.error);

    useEffect(() => {
        dispatch(fetchData());
    }, []);

    if (status === 'loading') {
        return (
            <Spin
                className="spinner theme"
                indicator={<LoadingOutlined spin />}
                size="large"
            />
        );
    }
    if (status === 'failed') {
        return (
            <>
                <h3 className="spinner">
                    Uh oh :&#40; <br /> <span>Something went wrong</span>
                    <p className="red">"{error}"</p>
                </h3>
            </>
        );
    }

    return (
        <div className="productList">
            {products.map((item) => (
                <Product key={item.id} product={item} />
            ))}
        </div>
    );
}

export default ProductList;
/* ProductList.css */

.productList {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
    padding: 20px;
}

.spinner {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.theme {
    color: #007a7a;
}

.red {
    color: #ff5656;
}
  1. Provide store in index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import Store from './redux/Store';

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

createAsyncThunk:

createAsyncThunk is a utility function provided by Redux Toolkit to handle asynchronous actions in a more structured and convenient way compared to manually writing thunks. It simplifies the process of defining, dispatching, and handling async actions, particularly with API calls or other side effects.

  1. Automatic Action Types: It generates pending, fulfilled, and rejected action types for the async operation.

  2. Payload Creator Function: A function where you can perform the async operation (e.g., API calls) and return the result.

  3. Lifecycle Actions: Dispatches actions automatically based on the state of the async operation (pending, fulfilled, rejected).

    • Async Thunk Definition:

      • fetchData is defined using createAsyncThunk.

      • It performs an asynchronous fetch operation.

      • If the fetch is successful, the data is returned and used as the action payload.

      • If the fetch fails, an error is thrown, and the rejected action is dispatched.

    • Slice Creation:

      • createSlice is used to create a slice of the state with extraReducers to handle the lifecycle actions (pending, fulfilled, rejected).

      • The state is updated based on the action type: loading during fetch, succeeded when data is fetched, and failed if there is an error.

    • Store Configuration:

      • configureStore from Redux Toolkit is used to set up the store with the reducer created by the slice.
    • Component Usage:

      • useDispatch is used to dispatch the fetchData thunk.

      • useSelector is used to read the state managed by the slice.

      • The component renders different UI based on the state (loading, error, or displaying data).

extraReducers:

extraReducers is a property you can define in a Redux Toolkit slice to handle actions that are not defined within the slice's reducers field. This is particularly useful when you need to handle actions generated by createAsyncThunk or actions from other slices.

  1. Handling Actions from createAsyncThunk: When you use createAsyncThunk, it generates three action types (pending, fulfilled, rejected) that you need to handle in your slice.

  2. Handling Actions from Other Slices: If you want a slice to respond to actions defined in other slices.

    • Builder Notation:

      • The extraReducers property uses a function that takes a builder object.

      • The builder object provides methods like addCase to define how the slice should handle specific action types.

      • This notation makes it clear which actions are being handled and avoids the need for switch-case statements.

    • Handling Thunk Actions:

      • addCase(fetchData.pending, ...): Handles the pending action dispatched by fetchData when the async operation starts.

      • addCase(fetchData.fulfilled, ...): Handles the fulfilled action dispatched by fetchData when the async operation completes successfully.

      • addCase(fetchData.rejected, ...): Handles the rejected action dispatched by fetchData when the async operation fails.

Redux Thunk simplifies handling asynchronous operations in Redux, making it easier to manage side effects and maintain clean, predictable state management in your React applications.

0
Subscribe to my newsletter

Read articles from Syed Aquib Ali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Syed Aquib Ali
Syed Aquib Ali

I am a MERN stack developer who has learnt everything yet trying to polish his skills 🥂, I love backend more than frontend.