Redux Thunk
Table of contents
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
- Install ReactJS
npx create-react-app redux-thunk
- 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.
- 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;
- Setup the store
import { configureStore } from '@reduxjs/toolkit';
import ProductReducer from './slices/ProductSlice';
import CartReducer from './slices/CartSlice';
export default configureStore({
reducer: {
ProductReducer,
CartReducer,
},
});
- 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 :( <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;
}
- 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.
Automatic Action Types: It generates
pending
,fulfilled
, andrejected
action types for the async operation.Payload Creator Function: A function where you can perform the async operation (e.g., API calls) and return the result.
Lifecycle Actions: Dispatches actions automatically based on the state of the async operation (pending, fulfilled, rejected).
Async Thunk Definition:
fetchData
is defined usingcreateAsyncThunk
.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 withextraReducers
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, andfailed
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 thefetchData
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.
Handling Actions from
createAsyncThunk
: When you usecreateAsyncThunk
, it generates three action types (pending
,fulfilled
,rejected
) that you need to handle in your slice.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 abuilder
object.The
builder
object provides methods likeaddCase
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 thepending
action dispatched byfetchData
when the async operation starts.addCase(fetchData.fulfilled, ...)
: Handles thefulfilled
action dispatched byfetchData
when the async operation completes successfully.addCase(fetchData.rejected, ...)
: Handles therejected
action dispatched byfetchData
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.
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.