Topic 11 of 56 · Full Stack Advanced

Actions and Reducers

Lesson TL;DRTopic 11: Actions and Reducers 📖 11 min read · 🎯 advanced · 🧭 Prerequisites: librariesimagepickerreactelements, restapis Why this matters Up until now, you've probably managed state inside individu...
11 min read·advanced·redux · state-management · actions · reducers

Topic 11: Actions and Reducers

📖 11 min read · 🎯 advanced · 🧭 Prerequisites: libraries-image-picker-react-elements, rest-apis

Why this matters

Up until now, you've probably managed state inside individual components — a useState here, a prop passed down there. That works fine for small things. But once your app grows, you start hitting a wall: one component updates something, another component doesn't know about it, and suddenly your UI shows three different "truths" at once. I've seen this break real projects. Redux solves this by giving your entire app a single, predictable place to manage state — and it does that through two simple ideas: actions (what happened) and reducers (what to do about it). That's what we're unpacking today.

What You'll Learn

  • Understand Redux's three core concepts: actions, reducers, and the store
  • Define action type constants and write action creator functions
  • Build a pure reducer that responds to dispatched actions with new state
  • Create a Redux store and wire it to a React app via the Provider component
  • Read state and dispatch actions from a React component using useSelector and useDispatch

The Analogy

Think of your application state as the official city ledger kept in the Vizag Records Office. No citizen can walk in and scribble directly on the ledger — that would cause chaos. Instead, you file a formal petition (an action) that describes exactly what you want changed: "Add one vote", "Remove one vote". A clerk called the reducer reads each petition, consults the current ledger, and produces a clean updated copy. The ledger itself (the store) is never mutated — only replaced with the clerk's freshly written copy. Every change is documented, every version traceable, and anyone auditing the ledger can replay the petitions to reconstruct any historical state.

Chapter 1: Introduction to Redux

Redux is a predictable state container for JavaScript applications. It helps you write applications that behave consistently and can run in different environments (client, server, native). Its entire surface area rests on three core concepts:

  1. Actions — Plain JavaScript objects that describe what happened. Every action must have a type property.
  2. Reducers — Pure functions that determine how the state changes in response to a received action.
  3. Store — The single object that holds the entire application state tree.

Because reducers are pure functions (no side effects, no mutation), the same action dispatched to the same state will always produce the same next state. This predictability is Redux's superpower for debugging and testing.

flowchart LR
    UI["React Component"] -->|"dispatch(action)"| Store
    Store -->|runs| Reducer
    Reducer -->|returns new state| Store
    Store -->|"useSelector()"| UI

Chapter 2: Setting Up Redux

The class bootstrapped a fresh project and installed both redux (the core library) and react-redux (the React bindings).

npm create vite@latest redux-example -- --template react
cd redux-example
npm install
npm install @reduxjs/toolkit react-redux

Modern Redux note: Create React App was officially sunset by the React team in 2025, and the bare redux package's createStore has been deprecated in favour of Redux Toolkit (configureStore + createSlice). This lesson teaches the classic action-types-plus-reducer-function shape so you can read existing codebases; see the Redux Toolkit Quick Start to migrate the same example to createSlice, which collapses everything below into about ten lines.

@reduxjs/toolkit re-exports legacy_createStore and the action/reducer primitives while adding the modern helpers — so we never need to install the bare redux package directly. react-redux provides the Provider component, useSelector, and useDispatch — the glue between Redux state and React components.

Chapter 3: Creating Actions

Actions are the vocabulary of your state machine. the trainer enforced a strict rule: always define action types as named constants first, then build action creators on top of them. This eliminates silent typo bugs where 'INCREMNT' dispatches to the void.

Defining Action Type Constants

src/actions/actionTypes.js:

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

By exporting string constants instead of bare string literals, both the action creator file and the reducer file import the same value — a typo in the constant name fails loudly at import time rather than silently at runtime.

Creating Action Creators

Action creators are functions that construct and return action objects. They keep your dispatch calls clean and testable.

src/actions/index.js:

import { INCREMENT, DECREMENT } from './actionTypes';

export const increment = () => {
    return {
        type: INCREMENT
    };
};

export const decrement = () => {
    return {
        type: DECREMENT
    };
};

increment() and decrement() each return a plain object with a type key. As your app grows, action creators can also accept arguments and attach a payload — for example, export const incrementBy = (amount) => ({ type: INCREMENT_BY, payload: amount }).

Chapter 4: Creating Reducers

A reducer is a pure function with the signature (state, action) => newState. It must never mutate the existing state — it returns a fresh object. the trainer wrote the counter reducer on the class whiteboard step by step.

src/reducers/counterReducer.js:

import { INCREMENT, DECREMENT } from '../actions/actionTypes';

const initialState = {
    count: 0
};

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

export default counterReducer;

Key rules enforced here:

  • Default parameter state = initialState handles the very first call when Redux initialises the store and passes undefined.
  • Spread operator ...state copies all existing state properties before overriding only what changed — this is how you avoid mutation.
  • default case always returns the current state unchanged, so unknown actions are a no-op rather than an error.

Chapter 5: Creating the Store

The store is created once and exported as a singleton. createStore accepts the root reducer (and optionally middleware, covered in the next lesson).

src/store.js:

import { legacy_createStore as createStore } from '@reduxjs/toolkit';
import counterReducer from './reducers/counterReducer';

const store = createStore(counterReducer);

export default store;

At this point the store holds { count: 0 }. Any call to store.dispatch({ type: 'INCREMENT' }) will trigger the reducer and update the store's state.

Chapter 6: Connecting Redux to React

React components don't interact with the store directly. Instead, react-redux provides a Provider component that makes the store available to any component in the tree via React context.

src/main.jsx (Vite's entry point; replaces CRA's src/index.js):

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

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

createRoot is the React 18+ entry API; ReactDOM.render was removed in React 19 and will throw on a fresh install.

Wrapping <App /> in <Provider store={store}> means every component rendered inside App — no matter how deeply nested — can subscribe to the Redux store without prop-drilling.

Chapter 7: Using Redux in React Components

With the store connected, any component can read state via useSelector and send actions via useDispatch.

Connecting a Component to the Redux Store

src/components/Counter.js:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../actions';

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

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

export default Counter;
  • useSelector((state) => state.count) subscribes to the store and re-renders the component whenever state.count changes.
  • useDispatch() returns the store's dispatch function. Calling dispatch(increment()) sends the action object { type: 'INCREMENT' } through the reducer.

Integrating the Counter Component

src/App.js:

import React from 'react';
import './App.css';
import Counter from './components/Counter';

function App() {
    return (
        <div className="App">
            <h1>Redux Counter Example</h1>
            <Counter />
        </div>
    );
}

export default App;

App stays clean — it simply renders Counter, which manages its own data connection through Redux hooks.

🧪 Try It Yourself

Task: Extend the counter with a RESET action that sets count back to 0.

  1. Add a RESET constant to src/actions/actionTypes.js.
  2. Add a reset action creator to src/actions/index.js.
  3. Add a case RESET to counterReducer.js that returns { ...state, count: 0 }.
  4. Add a Reset button to Counter.js that dispatches reset().

Success criterion: Clicking Increment several times then clicking Reset should display Count: 0 immediately in the browser — no page refresh needed.

Starter snippet for the reducer case:

case RESET:
    return {
        ...state,
        count: 0
    };

🔍 Checkpoint Quiz

Q1. Why do Redux reducers spread existing state (...state) instead of mutating it directly?

A) Because JavaScript doesn't allow property mutation B) Because Redux requires immutability so it can detect changes and notify subscribers efficiently C) Because useSelector only works with copies, not originals D) To save memory by sharing object references

Q2. Using the counter reducer defined in Chapter 4 (reproduced below) with initialState = { count: 5 }, what is the final value of count after these dispatches?

const counterReducer = (state = { count: 5 }, action) => {
    switch (action.type) {
        case 'INCREMENT': return { ...state, count: state.count + 1 };
        case 'DECREMENT': return { ...state, count: state.count - 1 };
        default: return state;
    }
};

store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'DECREMENT' });
store.dispatch({ type: 'INCREMENT' });

A) 5 B) 4 C) 6 D) 3

Q3. What is the purpose of the default case in a Redux reducer's switch statement?

A) It throws an error for unknown action types to catch bugs early B) It resets state to initialState when no case matches C) It returns the current state unchanged so unknown actions are no-ops D) It is required by JavaScript syntax and has no Redux-specific meaning

Q4. A CounterDisplay component needs to read state.count from the Redux store and dispatch a DECREMENT action when a button is clicked. Which pair of hooks should it use?

A) useState to mirror the count and useEffect to dispatch
B) useSelector to read state and useDispatch to send actions
C) useContext directly on the React-Redux context
D) useStore and useAction

A1. B — Redux's change-detection compares object references. If you mutate the existing object, the reference stays the same and Redux (and React) won't know anything changed.

A2. B — Starting at 5: DECREMENT → 4, DECREMENT → 3, INCREMENT → 4. Final count is 4.

A3. C — The default case is a safety net. Any action Redux dispatches internally (e.g., during store initialisation) won't match your custom types, and returning unchanged state ensures those internal dispatches are harmless no-ops.

A4. B — useSelector((state) => state.count) subscribes the component to the slice it needs; useDispatch() returns the dispatch function so the button can fire dispatch({ type: 'DECREMENT' }). useStore exists but is rarely needed in normal components, and useAction is not part of the React-Redux API.

🪞 Recap

  • Redux manages state through three primitives: actions (what happened), reducers (how state changes), and the store (where state lives).
  • Action type constants prevent silent typo bugs; action creators keep dispatch calls readable and testable.
  • Reducers are pure functions — they always return a new state object using ...spread rather than mutating the existing one.
  • createStore(reducer) initialises the store; <Provider store={store}> makes it available to the entire React tree.
  • useSelector reads state and triggers re-renders on change; useDispatch returns the function to fire actions.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.