DEV Community

Harsh Mishra
Harsh Mishra

Posted on

Redux: Intro + Building a Modern State Management System with Redux Toolkit

Mastering Redux in React: A Complete Guide to Building a Modern State Management System with Redux Toolkit

Introduction

Redux has long been a trusted solution for managing complex application state in React applications. Traditionally, using Redux involved a fair amount of boilerplate, leading to complexities that made state management cumbersome. To solve these challenges, Redux Toolkit was introduced as the official, recommended way to write Redux logic, simplifying setup, reducing boilerplate, and adding new features like slices and an easy-to-configure store.

This guide provides a comprehensive walkthrough of Redux Toolkit, covering everything from basic setup to creating an advanced project. We’ll build a shopping cart application using Redux Toolkit’s modern approach. By the end of this guide, you’ll have a solid understanding of:

  1. Setting up Redux Toolkit in a React project.
  2. Core Redux concepts (slices, actions, reducers).
  3. Best practices for structuring a complex Redux application.
  4. Building a feature-rich shopping cart project using Redux Toolkit.

Let’s dive in!


Why Redux Toolkit?

Redux Toolkit is designed to reduce the boilerplate code and complexity associated with Redux. Here are some benefits of Redux Toolkit:

  • Cleaner Code: Automatic creation of action types and reducers through slices.
  • Pre-configured Store: A store with essential middleware pre-applied.
  • Integrated DevTools: Redux DevTools integration for debugging.

Core Concepts of Redux Toolkit

Redux Toolkit streamlines Redux usage by reducing boilerplate code. It provides powerful utilities like createSlice and configureStore to make state management easier and more efficient. Let's explore these concepts in detail.

1. Slices: Structuring State and Actions Together

A slice is a Redux Toolkit concept that bundles the state and its reducers together into one cohesive unit. Each slice represents a specific part of your state, like "cart" or "user", and automatically generates actions based on your defined reducers.

Creating a Slice

To create a slice, we use createSlice from Redux Toolkit. It requires:

  • name: The slice name, which will be used as part of the action type.
  • initialState: The starting state for this slice.
  • reducers: A set of functions that describe how the state should be updated when an action is dispatched.
Example: Cart Slice
// features/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';

// Initial state for the cart
const initialState = {
  items: [],        // Array of cart items
  totalItems: 0,    // Total number of items
  totalPrice: 0.0,  // Total price of all items
};

// Create the cart slice
const cartSlice = createSlice({
  name: 'cart', // Name of the slice
  initialState,
  reducers: {
    addItem: (state, action) => {
      // Add a new item to the cart or increase quantity
      const existingItem = state.items.find(item => item.id === action.payload.id);
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      // Update totals
      state.totalItems += 1;
      state.totalPrice += action.payload.price;
    },
    removeItem: (state, action) => {
      const itemIndex = state.items.findIndex(item => item.id === action.payload.id);
      if (itemIndex >= 0) {
        state.totalItems -= state.items[itemIndex].quantity;
        state.totalPrice -= state.items[itemIndex].price * state.items[itemIndex].quantity;
        state.items.splice(itemIndex, 1); // Remove item from array
      }
    },
    updateItemQuantity: (state, action) => {
      const item = state.items.find(item => item.id === action.payload.id);
      if (item && action.payload.quantity > 0) {
        state.totalItems += action.payload.quantity - item.quantity;
        state.totalPrice += (action.payload.quantity - item.quantity) * item.price;
        item.quantity = action.payload.quantity;
      }
    },
  },
});

// Export the actions and reducer
export const { addItem, removeItem, updateItemQuantity } = cartSlice.actions;
export default cartSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Actions & Reducers: addItem, removeItem, and updateItemQuantity are defined as part of the slice. These actions automatically dispatch actions with the correct type and payload.
  • Immutable Updates: You don’t have to worry about immutability, as Redux Toolkit uses Immer internally to allow you to "mutate" the state directly.

2. Reducers and Actions: Simplifying Updates

In traditional Redux, you would manually define action types and action creators, and then use those to write your reducers. Redux Toolkit simplifies this by generating actions and reducers for you with createSlice.

How Redux Toolkit Generates Actions

When you define a reducer inside createSlice, Redux Toolkit automatically creates an action creator for each reducer function. For example:

  • The addItem reducer creates an action addItem, which can be dispatched from any component.

Example: Dispatching Actions from a Component

// CartComponent.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addItem, removeItem, updateItemQuantity } from './features/cartSlice';

function CartComponent() {
  const dispatch = useDispatch();
  const cartItems = useSelector((state) => state.cart.items);

  const handleAddItem = (product) => {
    dispatch(addItem(product)); // Dispatch the addItem action
  };

  const handleRemoveItem = (productId) => {
    dispatch(removeItem({ id: productId })); // Dispatch the removeItem action
  };

  const handleUpdateQuantity = (productId, quantity) => {
    dispatch(updateItemQuantity({ id: productId, quantity })); // Dispatch updateItemQuantity action
  };

  return (
    <div>
      <h2>Shopping Cart</h2>
      {cartItems.map(item => (
        <div key={item.id}>
          <p>{item.name}</p>
          <p>Price: ${item.price}</p>
          <button onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}>Increase</button>
          <button onClick={() => handleRemoveItem(item.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

export default CartComponent;
Enter fullscreen mode Exit fullscreen mode

Key Concepts:

  • dispatch: To trigger the action, you use dispatch() with the action creators generated by createSlice.
  • useSelector: Used to access the Redux store state. In this case, we get the list of cartItems from the Redux store.

With Redux Toolkit, the process is streamlined, and you no longer need to manually handle action types or create action creators.

3. The Redux Store: Combining Slices

The configureStore function from Redux Toolkit is used to combine all slices into a single store, making the entire Redux setup simpler and more efficient. This function automatically applies useful middleware (like Redux DevTools support and redux-thunk for asynchronous actions).

Example: Configuring the Redux Store

// store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './features/cartSlice';

const store = configureStore({
  reducer: {
    cart: cartReducer, // Combining the cart slice into the store
  },
});

export default store;
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • configureStore: Automatically includes Redux DevTools, thunk middleware, and simplifies the store setup by allowing you to pass in the reducer directly.
  • reducer: You can combine all your slices here. In this case, we have a cart slice that handles the state of the shopping cart.

Wrapping Up: Putting Everything Together

Finally, wrap your app with the Provider component from react-redux to make the Redux store available to all components in the app.

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

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode
  • createSlice combines state, reducers, and actions in one place, making the code easier to manage.
  • Reducers and Actions: In Redux Toolkit, reducers and actions are defined together within a slice, automatically generating actions for you.
  • configureStore simplifies store setup and integrates useful middleware and dev tools out of the box.

Setting Up Redux Toolkit in a React Project

To start using Redux Toolkit, let’s set up our project:

  1. Install Redux Toolkit and React-Redux in your project directory:
   npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode
  1. Set up the Redux store and connect it to your app.

Project Overview: Building a Shopping Cart

In this project, we’ll build a shopping cart application with the following features:

  • View Products: Display a list of products.
  • Add to Cart: Add products to the cart.
  • Remove from Cart: Remove products from the cart.
  • Update Quantities: Increase or decrease product quantities in the cart.
  • View Cart Summary: Display total items and total cost.

This project will allow us to understand how to manage complex state in Redux Toolkit while structuring our app for scalability.


Step 1: Setting Up Redux Store with Slices

Let’s begin by creating slices to manage our products and cart.

1. Create Product and Cart Slices

In a features folder, create productSlice.js and cartSlice.js.

Product Slice

We’ll start with a basic product slice to define and hold a list of products.

// features/productSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = [
  { id: 1, name: 'Laptop', price: 1000 },
  { id: 2, name: 'Phone', price: 500 },
  { id: 3, name: 'Headphones', price: 200 },
];

const productSlice = createSlice({
  name: 'products',
  initialState,
  reducers: {},
});

export default productSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Cart Slice

The cart slice will manage actions for adding, removing, and updating quantities of products in the cart.

// features/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: [],
  reducers: {
    addToCart: (state, action) => {
      const product = action.payload;
      const existingProduct = state.find((item) => item.id === product.id);
      if (existingProduct) {
        existingProduct.quantity += 1;
      } else {
        state.push({ ...product, quantity: 1 });
      }
    },
    removeFromCart: (state, action) => {
      const productId = action.payload;
      return state.filter((item) => item.id !== productId);
    },
    updateQuantity: (state, action) => {
      const { id, quantity } = action.payload;
      const product = state.find((item) => item.id === id);
      if (product && quantity > 0) {
        product.quantity = quantity;
      }
    },
  },
});

export const { addToCart, removeFromCart, updateQuantity } = cartSlice.actions;
export default cartSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Explanation

  • addToCart: Adds a product to the cart or increments the quantity if it already exists.
  • removeFromCart: Removes a product from the cart by its ID.
  • updateQuantity: Updates the quantity of a product if the quantity is greater than zero.

2. Configure the Redux Store

In a store.js file, configure the store to use our slices:

// store.js
import { configureStore } from '@reduxjs/toolkit';
import productReducer from './features/productSlice';
import cartReducer from './features/cartSlice';

const store = configureStore({
  reducer: {
    products: productReducer,
    cart: cartReducer,
  },
});

export default store;
Enter fullscreen mode Exit fullscreen mode

3. Provide Redux Store to the Application

In index.js, wrap the app in <Provider> and pass the store:

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

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

Step 2: Building the Shopping Cart Components

Now, let’s build out the components for the shopping cart.

1. ProductList Component

In ProductList.js, we’ll display the products and add a button to add items to the cart.

// ProductList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addToCart } from './features/cartSlice';

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

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            {product.name} - ${product.price}
            <button onClick={() => dispatch(addToCart(product))}>Add to Cart</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default ProductList;
Enter fullscreen mode Exit fullscreen mode

2. Cart Component

In Cart.js, we’ll display the items in the cart with options to update quantities and remove items.

// Cart.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { removeFromCart, updateQuantity } from './features/cartSlice';

function Cart() {
  const cart = useSelector((state) => state.cart);
  const dispatch = useDispatch();

  const handleQuantityChange = (id, quantity) => {
    if (quantity > 0) {
      dispatch(updateQuantity({ id, quantity }));
    }
  };

  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.map((item) => (
          <li key={item.id}>
            {item.name} - ${item.price} x {item.quantity}
            <input
              type="number"
              value={item.quantity}
              onChange={(e) => handleQuantityChange(item.id, +e.target.value)}
              min="1"
            />
            <button onClick={() => dispatch(removeFromCart(item.id))}>Remove</button>
          </li>
        ))}
      </ul>
      <CartSummary />
    </div>
  );
}

export default Cart;
Enter fullscreen mode Exit fullscreen mode

3. CartSummary Component

In CartSummary.js, we’ll calculate and display the total items and cost.

// CartSummary.js
import React from 'react';
import { useSelector } from 'react-redux';

function CartSummary() {
  const cart = useSelector((state) => state.cart);

  const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return (
    <div>
      <h3>Cart Summary</h3>
      <p>Total Items: {totalItems}</p>
      <p>Total Price: ${totalPrice.toFixed(2)}</p>
    </div>
  );
}

export default CartSummary;
Enter fullscreen mode Exit fullscreen mode

4. Integrate Components in the App

In App.js, render the ProductList and Cart components.

// App.js
import React from 'react';
import ProductList from './ProductList';
import Cart from './Cart';

function App() {
  return (


 <div>
      <h1>Redux Shopping Cart</h1>
      <ProductList />
      <Cart />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we covered the following:

  1. Setting up Redux Toolkit in a React application.
  2. Creating slices to manage products and cart state.
  3. Building components to interact with the Redux store.
  4. Structuring a shopping cart application to showcase complex state management with Redux.

By using Redux Toolkit, we built a robust, scalable application with minimal boilerplate, ensuring a modern and maintainable state management solution. This setup is suitable for applications of any size and can easily grow in complexity.

Top comments (0)