DAY 46: Mastered React-Redux, Slices, ImmerJS & Shopping Cart Project | ReactJS

Ritik KumarRitik Kumar
12 min read

🚀 Introduction

Welcome to Day 46 of my Web Development Journey!
After building a strong foundation in HTML, CSS, and JavaScript, I’ve been diving deep into ReactJS — a powerful library for creating dynamic and interactive user interfaces.

So far, I’ve explored state management in React using useState, Context API, useReducer, and Zustand. Recently, I began learning Redux, one of the most popular and robust libraries for managing state in modern web applications.

Over the past few days, I worked on:

  • Understanding the react-redux library and how it connects Redux with React
  • Implementing React-Redux from scratch
  • Learning about Slices and ImmerJS in Redux
  • Building a Shopping Cart Project using all the core concepts of Redux

📂 You can check out my complete Redux learning progress in my GitHub repository.

I’m documenting this journey publicly to stay consistent and share my learnings. Whether you’re just starting with React or aiming to strengthen your understanding of state management, I hope this blog adds value to your journey!

👉 Feel free to follow me on Twitter for real-time updates and coding insights.

📅 Here’s What I Covered Over the Last 3 Days

Day 43

  • React-Redux Library
  • Implement React-Redux Library from Scratch

Day 44

  • Slices in Redux
  • ImmerJS in Redux

Day 45

  • Built a Shopping Cart Project

Let’s dive into these concepts in detail below 👇


1. React-Redux Library

When working with React applications, managing state across multiple components can become complex.
This is where Redux comes in — a predictable state container. But to connect Redux with React components, we use the React-Redux library.

It provides hooks and utilities that make working with Redux in React much simpler.

What React-Redux Provides?

The React-Redux library offers several key features to integrate Redux with React effectively:

  1. Provider Component

    • Makes the Redux store available to all components in the React component tree.
    • No need to manually pass the store as props to every component.
  2. useSelector Hook

    • Allows components to access state from the Redux store.
    • Automatically subscribes to the store and re-renders the component when the selected state changes.
  3. useDispatch Hook

    • Provides a way to dispatch actions from React components.
    • Helps update the Redux store state in a predictable way.
  4. connect HOC (Higher-Order Component)

    • Older API to connect class components to Redux state and dispatch actions.
    • Still useful for legacy codebases, though hooks are preferred in modern React.
  5. Automatic Optimizations

    • Prevents unnecessary re-renders of components that do not depend on the changed state.
    • Efficiently updates only the components that actually use the updated state.
  6. Seamless Integration with Redux DevTools

    • Enables debugging, time-travel, and state inspection directly from the browser.

By providing these tools, React-Redux simplifies state management and ensures a clean, scalable approach to handling global state in React applications.

Installation:

To get started, install both redux and react-redux:

npm install redux react-redux

Setting Up Redux in React:

Here’s how we can set up Redux in a React application

1. Create a Redux Store

// store.js
import { createStore } from "redux";

// Initial state
const initialState = { count: 0 };

// Reducer function
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

// Create store
const store = createStore(counterReducer);

export default store;

2. Provide the Store to React

Wrap our app with the Provider component from react-redux:

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

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

Now every component inside <App /> has access to the Redux store.

Accessing Redux State in React:

To access state from the store, we use the useSelector hook:

// Counter.js
import React from "react";
import { useSelector } from "react-redux";

function Counter() {
  const count = useSelector((state) => state.count);

  return <h2>Count: {count}</h2>;
}

export default Counter;

Dispatching Actions in React:

To update state, we use the useDispatch hook to send actions to the store:

// CounterControls.js
import React from "react";
import { useDispatch } from "react-redux";

function CounterControls() {
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
    </div>
  );
}

export default CounterControls;

Final Thoughts:

  • React-Redux bridges the gap between React and Redux.
  • Provider makes the store accessible to the entire component tree.
  • useSelector lets you read state from the store.
  • useDispatch lets you trigger actions to update state.

2. Implement React-Redux Library from Scratch

To truly understand how React-Redux works under the hood, I tried implementing a simplified version of it.
This involves creating our own Provider, useSelector, and useDispatch using React’s Context API and hooks.

Code Implementation:

import { createContext, useContext, useEffect, useState } from "react";

const StoreContext = createContext();

export const Provider = ({ store, children }) => {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(store.getState());
    });

    return unsubscribe;
  }, [store]);

  return (
    <StoreContext.Provider value={{ state, dispatch: store.dispatch }}>
      {children}
    </StoreContext.Provider>
  );
};

export const useSelector = (selector) => {
  const { state } = useContext(StoreContext);
  return selector(state);
};

export const useDispatch = () => {
  const { dispatch } = useContext(StoreContext);
  return dispatch;
};

Explanation:

1. StoreContext

  • A React Context is created to provide global access to the Redux store.

2. Provider Component

  • Wraps the entire application and passes state and dispatch through context.
  • Uses useState to hold the current store state.
  • Subscribes to the Redux store inside useEffect so that whenever the store changes,
    the state is updated and components re-render.

3. useSelector Hook

  • A custom hook to read specific parts of the state.
  • Accepts a selector function, which extracts the required slice of state from the store.
const count = useSelector(state => state.counter);

4. useDispatch Hook

  • Provides direct access to the store’s dispatch function.
  • This allows components to dispatch actions easily.
const dispatch = useDispatch();
dispatch({ type: "INCREMENT" });

Final Thoughts:

By building React-Redux from scratch, I gain a deeper understanding of how it works internally:

  • Provider makes the Redux store available to React components.
  • useSelector extracts specific state values.
  • useDispatch allows dispatching actions to update the state.

3. Slices in Redux

In core Redux, the application state is often divided into different slices.
Each slice is managed by its own reducer function, and all reducers are combined together using the combineReducers function.

This makes state management modular, easier to maintain, and scalable as the app grows.

Example: Counter + Todo Reducers

import { combineReducers, createStore } from "redux";

// Counter Reducer
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// Todo Reducer
const todoReducer = (state = { todos: [] }, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return { todos: [...state.todos, action.payload] };
    default:
      return state;
  }
};

// Combine Reducers → Different slices of state
const rootReducer = combineReducers({
  counter: counterReducer,
  todo: todoReducer,
});

// Create Store
const store = createStore(rootReducer);

How the Store Looks?

After combining reducers, the Redux store has a nested structure:

{
  "counter": {
    "count": 0
  },
  "todo": {
    "todos": []
  }
}

Here:

  • counter slice manages the count state.
  • todo slice manages the list of todos.

Why Use Slices in Core Redux?

  • Modularity → Each reducer focuses on a specific feature.
  • Scalability → Easy to add new slices as app features grow.
  • Maintainability → Keeps logic separated and organized.

4. ImmerJS in Redux

When working with Redux, one of the most important rules is that state should be immutable.
This means we should never modify state directly, instead, we must return a new state object.

Manually handling immutability can sometimes be verbose and error-prone — especially when updating deeply nested objects.

This is where ImmerJS comes in.

What is ImmerJS?

Immer is a library that allows us to write mutable-looking code while keeping the state immutable under the hood.

It works by wrapping our state update logic in a produce function. Inside this function, we can write code as if we are mutating state directly. But Immer takes care of producing a new immutable state in the background.

Example:

Without Immer

// Immutable update
return { ...state, count: state.count + 1 };

Here, we use the spread operator to return a new object every time.

With Immer

// Mutable-looking update, but immutable under the hood
return produce(state, (draft) => {
  draft.count += 1;
});

With Immer, the code looks much cleaner, and we don’t have to worry about spreading or cloning objects manually.

How Immer Works?

Immer provides a function called produce.
It takes two arguments:

  1. Base State → Our current immutable state.
  2. Producer Function → A function that receives a draft (a proxy of the state) which we can update directly.

Immer then:

  • Creates a proxy (draft) of your state.
  • Lets us modify this draft as if it’s mutable.
  • Finalizes the draft and produces a brand-new immutable state.
  • The original state remains unchanged.

Example:

import produce from "immer";

const state = { todos: [{ text: "Learn Redux", done: false }] };

const nextState = produce(state, (draft) => {
  draft.todos[0].done = true; // Looks mutable
});

console.log(state.todos[0].done);     // false (original unchanged)
console.log(nextState.todos[0].done); // true (new immutable state)

Here, produce handles all the immutability under the hood —
we write simple, mutable-looking code, but still get immutable updates.

Benefits of Using Immer in Redux:

  • Cleaner Code → Write state updates in a natural, mutable style.
  • Less Error-Prone → Avoid accidental mutations and complex object spreads.
  • Great for Nested State → Simplifies updates on deeply nested objects/arrays.
  • Immutable Guarantee → Ensures Redux rules are followed without extra effort.

Final Thoughts:

  • Immer makes Redux reducers much easier to read and maintain.
  • It abstracts away the complexity of immutability while still keeping Redux’s core principle intact.

5. Build a Shopping Cart Project with Redux

This project is a simple shopping cart app built with React + Redux(core). It demonstrates how Redux helps manage global state for products, cart items, and wishlist.

1. Redux Store Setup

This is the store setup:

import { combineReducers, createStore } from "redux";
import productsReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import wishListReducer from "./slices/wishListSlice";

const rootReducer = combineReducers({
  products: productsReducer,
  cartItems: cartReducer,
  wishList: wishListReducer,
});

export const store = createStore(
  rootReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__?.()
);

Key Points:

  • combineReducers splits the state into slices:

    • products → list of all products
    • cartItems → items added to the cart
    • wishList → items marked as favorite
  • createStore creates the store with Redux DevTools enabled.

Store Shape looks like this:

{
  products: [...],      // product catalog
  cartItems: [...],     // array of cart objects
  wishList: [...]       // array of wishlist items
}

2. Products Slice

productsSlice.js is simple—it just holds the static product catalog:

export default function productsReducer(state = productsList) {
  return state;
}

No actions here, since products are static.

3. Cart Slice (Main Redux Logic)

The cart reducer manages adding/removing items and updating quantities.
It uses Immer (produce) to handle immutability.

import { produce } from "immer";

const CART_ADD_ITEM = "cart/addItem";
const CART_REMOVE_ITEM = "cart/removeItem";
const CART_ITEM_INCREASE_QUANTITY = "cart/increaseItemQuantity";
const CART_ITEM_DECREASE_QUANTITY = "cart/decreaseItemQuantity";

// Reducer
export default function cartReducer(originalState = [], action) {
  return produce(originalState, (state) => {
    const existingItemIndex = state.findIndex(
      (item) => item.productId === action.payload?.productId
    );

    switch (action.type) {
      case CART_ADD_ITEM:
        if (existingItemIndex !== -1) {
          state[existingItemIndex].quantity += 1;
          break;
        }
        state.push({ ...action.payload, quantity: 1 });
        break;

      case CART_REMOVE_ITEM:
        state.splice(existingItemIndex, 1);
        break;

      case CART_ITEM_INCREASE_QUANTITY:
        state[existingItemIndex].quantity += 1;
        break;

      case CART_ITEM_DECREASE_QUANTITY:
        state[existingItemIndex].quantity -= 1;
        if (state[existingItemIndex].quantity === 0) {
          state.splice(existingItemIndex, 1);
        }
        break;
    }
  });
}

Why This Works Well?

  • produce allows mutable-looking code while preserving immutability.
  • Handles multiple actions cleanly (add, remove, increase, decrease).
  • Cart state always stays consistent.

4. Wishlist Slice

Similar pattern, but simpler:

const WISHLIST_ADD_ITEM = "wishList/addItem";
const WISHLIST_REMOVE_ITEM = "wishList/removeItem";

export default function wishListReducer(state = [], action) {
  switch (action.type) {
    case WISHLIST_ADD_ITEM:
      return [...state, action.payload];
    case WISHLIST_REMOVE_ITEM:
      return state.filter(item => item.productId !== action.payload.productId);
    default:
      return state;
  }
}

Just adds/removes product IDs from wishlist.

5. Connecting Redux to React

Provider

In main.jsx:

<Provider store={store}>
  <RouterProvider router={router} />
</Provider>

This makes the Redux store available to all components.

6. Using Redux in Components

Product Component:

When user clicks Add to Cart, it dispatches addCartItem:

<button
  onClick={() =>
    dispatch(addCartItem({ productId, title, rating, price, imageUrl }))
  }>
  Add to Cart
</button>

Cart Component:

Cart page reads state with useSelector:

const cartItems = useSelector((state) => state.cartItems);

Then maps items to CartItem components.

CartItem Component:

Handles quantity updates

<button onClick={() => dispatch(decreaseCartItemQuantity(productId))}>-</button>
<span>{quantity}</span>
<button onClick={() => dispatch(increaseCartItemQuantity(productId))}>+</button>
<button onClick={() => dispatch(removeCartItem(productId))}>Remove</button>

Header Component:

Shows total cart count

const cartItems = useSelector((state) => state.cartItems);
{cartItems.reduce((sum, item) => sum + item.quantity, 0)}

7. Flow of Data in Redux

  1. User clicks Add to Cart → dispatches addCartItem.
  2. cartReducer updates state immutably with produce.
  3. React components subscribed via useSelector automatically re-render.
  4. Cart page & Header update instantly.

8. Final Thoughts

This project clearly shows why Redux is powerful:

  • Centralized state for cart & wishlist.
  • Pure reducers for predictable updates.
  • Immer keeps code clean and safe from mutation bugs.
  • React + Redux integration is seamless with useSelector and useDispatch.

6. What’s Next:

I’m excited to keep growing and sharing along the way! Here’s what’s coming up:

  • Posting new blog updates every 3 days to share what I’m learning and building.
  • Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
  • Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!

Thanks for being part of this journey!


7. Conclusion:

Over the last three days, I’ve gone from understanding the basics of Redux to building a complete Shopping Cart project with it.
This journey taught me not only how Redux works in theory, but also how it solves real-world state management problems in React apps.

Key takeaways from this learning:

  • Redux provides a centralized state container, making data flow predictable and easier to debug.
  • Writing reducers with Immer simplifies immutable updates and keeps the code clean.
  • React-Redux’s Provider, useSelector, and useDispatch make integrating Redux into React seamless.
  • By splitting state into slices, we can scale applications in a structured and modular way.

Learning Redux wasn’t just about mastering a tool — it was about building the right mindset for handling complex state management.
With this foundation, I feel more confident in tackling larger projects and optimizing React applications.

🔥 Next, I’ll continue exploring more advanced Redux concepts and maybe dive into Redux Toolkit (RTK), which makes Redux even more developer-friendly.

Thanks for reading! Feel free to connect or follow along as I continue building and learning in React.

0
Subscribe to my newsletter

Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ritik Kumar
Ritik Kumar

👨‍💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.