Skip to main content

Overview

Redux is a predictable state container for JavaScript applications. It helps manage application state in a centralized store, making state changes traceable and debuggable.

Redux Store

Single source of truth for application state

Actions

Describe what happened in the app

Reducers

Specify how state changes

Redux Toolkit

Modern, efficient Redux development

Why Redux?

  • Prop Drilling: No need to pass state through many components
  • State Management: Centralized state easier to manage
  • Predictability: State changes follow strict patterns
  • Debugging: Time-travel debugging with dev tools
  • Scalability: Works well in large applications
Consider Redux when:
  • App has complex state logic
  • State is needed in many places
  • State updates follow complex patterns
  • You need powerful debugging tools
  • Team prefers explicit state management
You might NOT need Redux for:
  • Simple apps with minimal state
  • State mostly local to components
  • Quick prototypes

Core Concepts

The single source of truth holding application state:
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    auth: authReducer,
  },
});
The store:
  • Holds entire application state
  • Allows state access via getState()
  • Updates state via dispatch(action)
  • Registers listeners via subscribe()

Redux Toolkit (RTK)

Redux Toolkit is the official, recommended way to write Redux logic. It simplifies common patterns and includes best practices.
1

Install Redux Toolkit

npm install @reduxjs/toolkit react-redux
2

Create a Slice

Slices contain reducer logic and actions for a feature
3

Configure Store

Combine slices into a single store
4

Provide Store

Make store available to React components
5

Use in Components

Read state with useSelector and dispatch with useDispatch

Creating a Slice

Slices are the core of Redux Toolkit, combining actions and reducers.
import { createSlice } from '@reduxjs/toolkit';

const initialCounterState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name: 'counter',
  initialState: initialCounterState,
  reducers: {
    increment(state) {
      // Looks like mutation, but Immer handles it!
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter = state.counter + action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

export const counterActions = counterSlice.actions;
export default counterSlice.reducer;
createSlice Benefits:
  • Automatically generates action creators
  • Uses Immer for “mutable” updates
  • Reduces boilerplate code
  • Type-safe with TypeScript

Configuring the Store

Combine all slices into a single store:
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counter';
import authReducer from './auth';

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

export default store;
With this configuration, state looks like:
{
  counter: {
    counter: 0,
    showCounter: true
  },
  auth: {
    isAuthenticated: false
  }
}
configureStore automatically:
  • Sets up Redux DevTools
  • Adds middleware (thunk, etc.)
  • Enables development checks
  • Combines reducers

Providing the Store

Make the store available to your React app:
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store/index';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
The Provider component must wrap your entire app to give all components access to the store.

Using Redux in Components

Access state and dispatch actions using React-Redux hooks.
Read data from the store:
import { useSelector } from 'react-redux';

const counter = useSelector((state) => state.counter.counter);
const show = useSelector((state) => state.counter.showCounter);
const isAuth = useSelector((state) => state.auth.isAuthenticated);
useSelector automatically subscribes to store updates. Component re-renders when selected state changes.

Complete Component Example

Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { counterActions } from '../store/counter';
import classes from './Counter.module.css';

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter.counter);
  const show = useSelector((state) => state.counter.showCounter);

  const incrementHandler = () => {
    dispatch(counterActions.increment());
  };

  const increaseHandler = () => {
    dispatch(counterActions.increase(10));
  };

  const decrementHandler = () => {
    dispatch(counterActions.decrement());
  };

  const toggleCounterHandler = () => {
    dispatch(counterActions.toggleCounter());
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      {show && <div className={classes.value}>{counter}</div>}
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={increaseHandler}>Increase by 10</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

Action Payloads

Pass data to actions via payloads:
const counterSlice = createSlice({
  name: 'counter',
  initialState: { counter: 0 },
  reducers: {
    increase(state, action) {
      // action.payload contains the data
      state.counter = state.counter + action.payload;
    },
  },
});
Redux Toolkit Payload Convention:When you dispatch counterActions.increase(10), RTK creates:
{
  type: 'counter/increase',
  payload: 10
}
For multiple values, pass an object:
dispatch(counterActions.updateUser({ id: 1, name: 'John' }));

Working with Multiple Slices

Organize related state into separate slices:
App.js
import { useSelector } from 'react-redux';
import Counter from './components/Counter';
import Auth from './components/Auth';
import Header from './components/Header';
import UserProfile from './components/UserProfile';

function App() {
  const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);

  return (
    <>
      <Header />
      {!isAuthenticated && <Auth />}
      {isAuthenticated && <UserProfile />}
      <Counter />
    </>
  );
}

export default App;
Slice Organization:Create separate slices for different features:
  • userSlice - User profile and preferences
  • cartSlice - Shopping cart
  • authSlice - Authentication state
  • uiSlice - UI state (modals, notifications)
Each slice is independent and focused on one concern.

Redux Immutability

Manually create new state objects:
function counterReducer(state, action) {
  if (action.type === 'increment') {
    // ✅ Create new object
    return {
      ...state,
      counter: state.counter + 1
    };
  }
  
  // ❌ NEVER mutate state
  // state.counter++;
  // return state;
}

Class-Based Components

While the course focuses on hooks, Redux also works with class components:
import { Component } from 'react';
import { connect } from 'react-redux';

class Counter extends Component {
  incrementHandler() {
    this.props.increment();
  }

  render() {
    return (
      <div>
        <div>{this.props.counter}</div>
        <button onClick={this.incrementHandler.bind(this)}>
          Increment
        </button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    counter: state.counter.counter,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch({ type: 'counter/increment' }),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Modern React apps should use hooks (useSelector and useDispatch) instead of connect. The connect HOC is legacy but still supported.

Redux DevTools

Redux Toolkit automatically enables Redux DevTools:
1

Install Extension

Install Redux DevTools browser extension
2

View State

Inspect current state structure and values
3

Track Actions

See all dispatched actions in order
4

Time Travel

Jump to any previous state
5

Debug Issues

Understand state changes and find bugs

Best Practices

Organize by Feature

Group related slices, components, and logic together

Normalize State

Store data in flat structure, not deeply nested

Use Redux Toolkit

Avoid hand-written Redux - use RTK always

Selective Selectors

Select only the state you need to minimize re-renders

Action Naming

Use clear, descriptive action names

Keep Logic in Reducers

Reducers contain business logic, components stay simple

Common Patterns

const dataSlice = createSlice({
  name: 'data',
  initialState: {
    items: [],
    loading: false,
    error: null,
  },
  reducers: {
    fetchStart(state) {
      state.loading = true;
      state.error = null;
    },
    fetchSuccess(state, action) {
      state.loading = false;
      state.items = action.payload;
    },
    fetchError(state, action) {
      state.loading = false;
      state.error = action.payload;
    },
  },
});
reducers: {
  toggleSidebar(state) {
    state.sidebarOpen = !state.sidebarOpen;
  },
  openModal(state) {
    state.modalOpen = true;
  },
  closeModal(state) {
    state.modalOpen = false;
  },
}
reducers: {
  addItem(state, action) {
    state.items.push(action.payload);
  },
  removeItem(state, action) {
    state.items = state.items.filter(
      (item) => item.id !== action.payload
    );
  },
  updateItem(state, action) {
    const index = state.items.findIndex(
      (item) => item.id === action.payload.id
    );
    if (index !== -1) {
      state.items[index] = action.payload;
    }
  },
}

Resources

Redux Toolkit Docs

Official Redux Toolkit documentation

React-Redux Hooks

useSelector and useDispatch API reference

Build docs developers (and LLMs) love