Skip to main content

Redux & State Management

As your React application grows beyond a few components, you’ll encounter challenges with state management. Props drilling becomes unwieldy, Context API re-renders too much, and tracking state changes becomes a nightmare. Redux addresses these problems with a structured approach to global state.

When Do You Need Redux?

Redux adds complexity, so it’s important to know when it’s worth it:

You Might Need Redux When:

  • Multiple components need access to the same state
  • State updates come from many different sources (user input, API calls, WebSockets)
  • State changes are complex with many interconnected pieces
  • Debugging is difficult because you can’t track what changed and when
  • Team collaboration requires predictable patterns everyone follows

You Probably Don’t Need Redux When:

  • Your app is simple with few components
  • State is localized to individual components
  • Context API handles your needs without performance issues
  • You’re building a prototype or MVP
Start with React’s built-in state management (useState, useContext). Only add Redux when you feel genuine pain from state complexity.

Redux Core Concepts

Before diving into code, understand the philosophy:
ConceptDescription
Single Source of TruthAll app state lives in one store object
State is Read-OnlyThe only way to change state is by dispatching actions
Pure ReducersChanges are made with pure functions (given same input, always same output)
This predictability makes Redux apps easier to debug, test, and reason about.

Redux Toolkit (RTK)

We’ll use Redux Toolkit (RTK), the official, recommended way to write Redux logic. It eliminates boilerplate and includes best practices by default.

Installation

npm install @reduxjs/toolkit react-redux

1. Create a Slice

A “slice” contains the reducer logic and actions for a single feature. features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers.
      // It doesn't actually mutate the state because it uses the Immer library.
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

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

export default counterSlice.reducer;

2. Configure the Store

Create the Redux store and add your slices. app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

3. Provide the Store

Wrap your application with the Provider. main.jsx
import { store } from './app/store';
import { Provider } from 'react-redux';

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

4. Use State and Dispatch

Use useSelector to read data and useDispatch to dispatch actions. Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './features/counter/counterSlice';

export function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <span>{count}</span>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

Async Logic (Thunks)

Redux Toolkit includes createAsyncThunk for handling async logic (like API calls).
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

export const fetchUser = createAsyncThunk(
  'users/fetchById',
  async (userId, thunkAPI) => {
    const response = await fetch(\`/api/users/\${userId}\`);
    return response.json();
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  extraReducers: (builder) => {
    builder.addCase(fetchUser.fulfilled, (state, action) => {
      state.entities.push(action.payload);
    });
  },
});

Other Options

While Redux is popular, other libraries exist:
  • Zustand: Minimalist, hook-based state management.
  • Recoil / Jotai: Atomic state management.
  • TanStack Query (React Query): Best for server state (caching API responses).

Summary

  • Redux Toolkit simplifies Redux setup.
  • Slices organize logic by feature.
  • Provider makes the store available to the app.
  • useSelector reads state.
  • useDispatch sends actions to update state.