Skip to main content

Command Palette

Search for a command to run...

Understanding Redux: Taking State Management to the Next Level (Part 2)

Updated
8 min read
Understanding Redux: Taking State Management to the Next Level (Part 2)

Introduction

Welcome back! In Part 1, we learned about useState for local state and Context API for sharing state across components. Now, let's explore Redux - a powerful state management solution for larger, more complex applications.

Quick Recap: What We've Learned

  • useState: Great for single component state

  • Context API: Solves prop drilling, shares state across components

  • The Question: So why do we need Redux?

The Problem Redux Solves

When Context API Becomes Challenging

Context API is awesome, but imagine you're building a large app like Facebook or Amazon. You might have:

  • User authentication state

  • Shopping cart state

  • Notification state

  • Theme preferences

  • Language settings

  • Product filters

  • Search history

  • And much more...

Problems that emerge:

  1. Too Many Contexts: You end up with 10+ different context providers wrapped around your app

  2. Performance Issues: When context updates, ALL components using it re-render (even if they don't need to)

  3. Complex Logic: Your state update functions become complicated and hard to test

  4. Debugging Difficulty: Hard to track what changed the state and when

Enter Redux

Redux is like hiring a professional warehouse manager for your state. Everything is organized, trackable, and efficient.

Understanding Redux: The Core Concepts

Think of Redux as a library with strict rules. These rules might seem annoying at first, but they make your code predictable and easy to debug.

The Three Principles of Redux

  1. Single Source of Truth: All your app's state lives in ONE place (the store)

  2. State is Read-Only: You can't directly change state - you must send a request (action)

  3. Changes via Pure Functions: State changes happen through predictable functions (reducers)

The Redux Flow: A Real-World Analogy

Imagine a restaurant:

Customer (Component)
    ↓
Orders food (Dispatches Action)
    ↓
Waiter takes order to kitchen (Middleware - optional)
    ↓
Chef prepares food (Reducer)
    ↓
Updated meal (New State)
    ↓
Served to customer (Component Re-renders)

Let's break down each part:

Part 1: The Store (The Warehouse)

The store is where ALL your application state lives. Instead of scattered useState hooks everywhere, everything is in one place.

// store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    // user: userReducer,
    // cart: cartReducer,
    // You can add more here as your app grows
  }
});

Think of this as your warehouse with different sections (counter, user, cart).

Part 2: Actions (The Orders)

Actions are plain objects that describe WHAT happened. They're like order slips.

// An action looks like this:
{
  type: 'counter/increment',
  payload: 1  // optional data
}

But you don't write these manually! Redux Toolkit makes it easier.

Part 3: Slices (Organized Sections)

A slice is a collection of Redux logic for a specific feature. Redux Toolkit introduced slices to make Redux simpler.

// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // You can write "mutating" logic here!
      // Redux Toolkit uses Immer under the hood
      state.value += 1;
    },
    decrement: (state) => {
      state.value = state.value > 0 ? state.value - 1 : 0;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
    }
  }
});

export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;

What's Happening Here?

  1. name: Identifies this slice (like a label on a warehouse section)

  2. initialState: The starting state (empty warehouse)

  3. reducers: Functions that update state (warehouse operations)

Important: Even though it LOOKS like we're mutating state (state.value += 1), Redux Toolkit uses a library called Immer that makes this safe. Behind the scenes, it's still immutable!

Part 4: Setting Up Redux in Your App

Step 1: Wrap Your App with Provider

Just like Context API, you need a Provider:

// pages/_app.tsx
import { Provider } from 'react-redux';
import { store } from '@/store/store';

export default function App({ Component, pageProps }) {
  return (
    <Provider store={store}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Provider>
  );
}

Step 2: Create Typed Hooks (TypeScript Bonus)

These hooks make using Redux with TypeScript easier:

// store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Step 3: Update Your Store Types

// store/store.ts
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Part 5: Using Redux in Components

Now comes the magic! Using Redux in your components is clean and simple.

Reading State (useSelector)

// components/CounterDisplay.tsx
import { useAppSelector } from '@/store/hooks';

const CounterDisplay = () => {
  // Select only the data you need
  const count = useAppSelector((state) => state.counter.value);

  return (
    <div>
      <h1>Current Count: {count}</h1>
    </div>
  );
}

Updating State (useDispatch)

// pages/counter-app.tsx
import { useAppSelector, useAppDispatch } from '@/store/hooks';
import { increment, decrement, reset } from '@/store/counterSlice';

const CounterApp = () => {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => dispatch(increment())}>
        Increment
      </button>

      <button onClick={() => dispatch(decrement())}>
        Decrement
      </button>

      <button onClick={() => dispatch(reset())}>
        Reset
      </button>
    </div>
  );
}

In the Header Component

// components/layouts/Header.tsx
import { useAppSelector } from '@/store/hooks';

const Header = () => {
  const count = useAppSelector((state) => state.counter.value);

  return (
    <header>
      <p>Current count: {count}</p>
    </header>
  );
}

The Redux Flow in Our Counter App

Let's trace what happens when you click "Increment":

  1. User clicks buttononClick={() => dispatch(increment())}

  2. Action is dispatched{ type: 'counter/increment' }

  3. Redux calls the reducer → The increment function in counterSlice

  4. State updatesstate.value += 1

  5. Components re-render → Only components using that state update!

Comparing All Three Approaches

useState

const [count, setCount] = useState(0);
// Pro: Simple and direct
// Con: Can't share easily

Context API

const { count, increment } = useCount();
// Pro: Shares across components
// Con: All consumers re-render on any change

Redux

const count = useAppSelector(state => state.counter.value);
const dispatch = useAppDispatch();
// Pro: Shares across components, optimized re-renders, great dev tools
// Con: More setup code

When to Use Redux?

✅ Use Redux When:

  • Building a large application with lots of state

  • Many components need access to the same state

  • State logic is complex (multiple related pieces of state)

  • You need powerful debugging tools

  • You want time-travel debugging (undo/redo)

  • Working with a team on a big project

  • You need middleware for side effects (API calls)

❌ Don't Use Redux When:

  • Building a small app or prototype

  • State is simple and local

  • Context API is working fine

  • You're just learning React (start simpler!)

Redux DevTools: The Superpower

One of Redux's best features is the Redux DevTools browser extension. It lets you:

  • See every action that was dispatched

  • View the state before and after each action

  • Time-travel through your state changes

  • Export and import state for debugging

It's like having a security camera recording everything that happens in your app!

Advanced Redux Concepts (Brief Overview)

As you grow with Redux, you'll encounter:

Async Actions with Thunks

For handling API calls and asynchronous logic:

// Example structure (don't worry about details yet)
const fetchUserData = createAsyncThunk(
  'user/fetchData',
  async (userId) => {
    const response = await fetch(`/api/user/${userId}`);
    return response.json();
  }
);

Middleware

Functions that run between dispatching an action and reaching the reducer. Great for logging, async operations, etc.

Selectors with Reselect

Optimized functions to compute derived data from state without recalculating unnecessarily.

Best Practices

  1. Keep reducers pure: No side effects, no API calls in reducers

  2. Normalize complex state: Store data in a flat structure

  3. Use Redux Toolkit: It's the official, recommended way

  4. Select only what you need: Don't select the entire state

  5. Organize by feature: Group related slices together

  6. Write meaningful action names: user/login not user/update

Common Beginner Mistakes

  1. Putting everything in Redux: Not all state needs Redux (form inputs can stay local!)

  2. Forgetting the Provider: App won't work without it

  3. Mutating state directly: Always use the reducer functions

  4. Not using Redux Toolkit: The old way is much harder

  5. Over-engineering small apps: Start simple, add Redux when needed

Progression Summary

Stage 1: useState
↓ (Need to share state)
Stage 2: Context API
↓ (App getting complex, need better debugging, performance issues)
Stage 3: Redux

You don't jump straight to Redux! Learn each step and use the right tool for the job.

Practical Example: Expanding Our Counter

Let's say your app grows to need:

  • A counter

  • User authentication

  • Shopping cart

  • Notifications

With Redux:

// store/store.ts
export const store = configureStore({
  reducer: {
    counter: counterReducer,
    auth: authReducer,
    cart: cartReducer,
    notifications: notificationsReducer
  }
});

// Now any component can access any slice:
const count = useAppSelector(state => state.counter.value);
const user = useAppSelector(state => state.auth.user);
const cartItems = useAppSelector(state => state.cart.items);

All organized, all in one place, all easily accessible!

Conclusion

Redux might seem like overkill when you first learn it, and that's okay! It's a professional tool for professional-scale problems. Here's your journey:

  1. Master useState → Handle local component state

  2. Learn Context API → Share state, avoid prop drilling

  3. Adopt Redux → Scale to complex applications

Redux gives you:

  • 🏗️ Structure: Clear patterns for organizing code

  • 🐛 Debugging: Amazing developer tools

  • 📈 Scalability: Handles growth from 10 to 10,000 components

  • 🔄 Predictability: State changes are always traceable

  • 👥 Team Collaboration: Everyone follows the same patterns

Start with what you know, grow into what you need. Redux will be there when you're ready to level up! 🚀

Next Steps

  1. Build the counter app with all three approaches

  2. Compare the code complexity

  3. Try Redux DevTools to see the magic

  4. Build a todo list app with Redux

  5. Explore Redux Toolkit's documentation

Remember: The best state management solution is the one that fits your project's needs. Don't use Redux just because it's popular - use it because it solves real problems in your application!

Happy coding! 🎉

More from this blog

S

Salome Githinji - Tech Hub

18 posts