Topic 11 of 15 · React Developer

Actions and Reducers

Lesson TL;DRTopic 11: Actions and Reducers 📖 5 min read · 🎯 advanced · 🧭 Prerequisites: hooks, introductiontoredux Why this matters Up until now, your React components have each been managing their own state —...
5 min read·advanced·redux · state-management · actions · reducers

Topic 11: Actions and Reducers

📖 5 min read · 🎯 advanced · 🧭 Prerequisites: hooks, introduction-to-redux

Why this matters

Up until now, your React components have each been managing their own state — and that works fine for small apps. But picture this: your app grows, you've got ten components, and they all need to share the same data. Who owns it? Who updates it? Why does clicking a button in one place break something completely unrelated? I've seen beginners spend hours on bugs that weren't even bugs — just state getting out of sync. Redux solves exactly this. And the two things that make Redux actually work — that make it predictable — are actions and reducers. That's what we're digging into today.

What You'll Learn

  • What Redux actions are and how to define action types as constants
  • How to write action creator functions that produce action objects
  • How to write a reducer that responds to actions and returns new state
  • How to create a Redux store and wire it into a React app with Provider
  • How to read Redux state and dispatch actions from a React component using useSelector and useDispatch

The Analogy

Think of your application state as a city's treasury. Nobody is allowed to walk in and grab money or dump bags of coins on the desk — every transaction must go through an official request form (the action) describing exactly what is happening: "Deposit 50", "Withdraw 20". A single treasurer (the reducer) receives every form, looks at the current balance, applies the change by the book, and returns a new official ledger — never crossing out numbers on the old one, always producing a fresh page. The store is the vault that keeps the current ledger and makes sure the treasurer is the only one who can update it. This paper-trail discipline is exactly why Redux bugs are so easy to trace: every state change has a timestamped form to match.

Chapter 1: Introduction to Redux

Redux is a predictable state container for JavaScript applications. It helps you write applications that behave consistently across client, server, and native environments, and its strict data-flow makes state changes easy to reproduce and debug.

Core Concepts of Redux

  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 given action.
  3. Store — A single object that holds the complete application state tree.
flowchart LR
    UI["React Component"] -->|"dispatch(action)"| Store
    Store -->|"action"| Reducer
    Reducer -->|"new state"| Store
    Store -->|"state via useSelector"| UI

Chapter 2: Setting Up Redux

The class scaffolded a fresh React app and added the two packages they needed: redux (the state engine) and react-redux (the React bindings).

npx create-react-app redux-example
cd redux-example
npm install redux react-redux

After installation the relevant parts of the project tree look like this:

src/
  actions/
    actionTypes.js
    index.js
  reducers/
    counterReducer.js
  components/
    Counter.js
  store.js
  index.js
  App.js

Chapter 3: Creating Actions

An action is just a plain JavaScript object with a type field. Everything else is convention — but strong convention, which is why the class follows it.

Defining Action Types

Hardcoding strings like 'INCREMENT' in multiple files is a typo waiting to happen. Centralising them as named constants means a misspelling becomes an immediate undefined reference error instead of a silent bug.

src/actions/actionTypes.js

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

Creating Action Creators

Action creators are functions that construct and return action objects. Using them means you never hand-write action literals in component code — you call a function, and the shape is always correct.

src/actions/index.js

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

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

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

Why functions instead of raw objects? Because when actions carry a payload (e.g., increment(5)) you only add the parameter to the creator — every call site automatically gets the right shape.

Chapter 4: Creating Reducers

A reducer is a pure function with the signature (state, action) => newState. It must never mutate the existing state — it must return a brand-new object. This immutability is what makes time-travel debugging and reliable change detection possible.

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 points:

  • The state = initialState default parameter handles the very first call, when Redux passes undefined.
  • The spread operator (...state) copies all existing fields before overriding just count, preserving any other state keys you might add later.
  • The default case always returns the current state unchanged — this is required, because Redux dispatches internal setup actions your reducers must silently ignore.

Chapter 5: Creating the Store

The store is created once, at the top of the app, by passing the root reducer to createStore.

src/store.js

import { createStore } from 'redux';
import counterReducer from './reducers/counterReducer';

const store = createStore(counterReducer);

export default store;

The store object exposes three key methods: getState() (read current state), dispatch(action) (trigger a state change), and subscribe(listener) (react to changes). In practice, react-redux hooks call these for you so you rarely use them directly.

Chapter 6: Connecting Redux to React

To make the store available anywhere in the component tree, the class wrapped the entire app in the Provider component from react-redux. Provider uses React context under the hood, so no manual prop-drilling is required.

src/index.js

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

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

Everything rendered inside <Provider> can now access the Redux store. Nothing outside it can — which is why Provider must be the outermost wrapper.

Chapter 7: Using Redux in React Components

With the store wired up, individual components can read state and dispatch actions using two hooks from react-redux.

HookPurpose
useSelector(selector)Reads a slice of Redux state; re-renders only when that slice changes
useDispatch()Returns the store's dispatch function

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;

The useSelector selector function receives the full Redux state tree and returns only the piece the component needs. If other parts of the state change but state.count stays the same, this component will not re-render — an important performance characteristic.

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;

🧪 Try It Yourself

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

  1. Add export const RESET = 'RESET'; 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 and then Reset should immediately display Count: 0 in the browser without a page reload.

Starter snippet for the action creator:

export const reset = () => {
    return {
        type: RESET
    };
};

🔍 Checkpoint Quiz

Q1. Why are action type strings stored as named constants in a separate file rather than written inline as string literals?

A) Redux requires it to function correctly
B) It prevents typos from creating silent bugs and makes refactoring easier
C) String literals are not valid JavaScript object values
D) It improves runtime performance

Q2. Given this reducer, what is the value of state.count after dispatching { type: 'INCREMENT' } twice followed by { type: 'DECREMENT' } once?

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;
    }
};

A) 0
B) 1
C) 2
D) 3

Q3. What is wrong with this reducer implementation?

const buggyReducer = (state = { count: 0 }, action) => {
    if (action.type === 'INCREMENT') {
        state.count += 1;
        return state;
    }
    return state;
};

A) It uses if instead of switch
B) It mutates the existing state object instead of returning a new one
C) It does not export the function
D) The initial state must be a primitive, not an object

Q4. A component only needs to display the current count from Redux state but never dispatch actions. Which hook(s) should it use, and why?

A1. B — Constants centralise the string in one place. A typo in a string literal dispatches an action with an unrecognised type, the default case returns unchanged state, and the bug is completely silent. A typo in a constant reference throws a ReferenceError immediately.

A2. B — Starting at 0, INCREMENT → 1, INCREMENT → 2, DECREMENT → 1.

A3. B — The reducer directly mutates state.count on the existing object and then returns the same reference. React-Redux detects changes by reference equality; returning the same object reference means the store thinks nothing changed and connected components will not re-render. The fix is return { ...state, count: state.count + 1 }.

A4. Only useSelector. It subscribes the component to a slice of Redux state and triggers a re-render when that slice changes. useDispatch is only needed when the component needs to send actions to the store — a read-only component has no reason to call it.

🪞 Recap

  • Actions are plain objects with a type field; action creators are functions that build them consistently.
  • Reducers are pure functions — they must never mutate existing state, always returning a new object.
  • The Redux store is created once via createStore(reducer) and made available app-wide by wrapping the root component in <Provider store={store}>.
  • useSelector reads a slice of state; useDispatch returns the function to send actions — together they replace the older connect() HOC pattern.
  • Moving action type strings to named constants prevents silent typo bugs and centralises maintenance.

📚 Further Reading

Like this topic? It’s one of 15 in React Developer.

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