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
Providercomponent - Read state and dispatch actions from a React component using
useSelectoranduseDispatch
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:
- Actions — Plain JavaScript objects that describe what happened. Every action must have a
typeproperty. - Reducers — Pure functions that determine how the state changes in response to a received action.
- 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
reduxpackage'screateStorehas 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 tocreateSlice, 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 = initialStatehandles the very first call when Redux initialises the store and passesundefined. - Spread operator
...statecopies all existing state properties before overriding only what changed — this is how you avoid mutation. defaultcase 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 wheneverstate.countchanges.useDispatch()returns the store'sdispatchfunction. Callingdispatch(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.
- Add a
RESETconstant tosrc/actions/actionTypes.js. - Add a
resetaction creator tosrc/actions/index.js. - Add a
case RESETtocounterReducer.jsthat returns{ ...state, count: 0 }. - Add a Reset button to
Counter.jsthat dispatchesreset().
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
...spreadrather than mutating the existing one. createStore(reducer)initialises the store;<Provider store={store}>makes it available to the entire React tree.useSelectorreads state and triggers re-renders on change;useDispatchreturns the function to fire actions.
📚 Further Reading
- Redux Official Docs — the source of truth on actions, reducers, and store API
- React-Redux Hooks API — complete reference for
useSelector,useDispatch, anduseStore - Redux Style Guide — the official opinionated best-practices guide (action naming, reducer structure, selector patterns)
- ⬅️ Previous: REST APIs
- ➡️ Next: Redux Toolkit Quick Start (external — the recommended next step for any new Redux app)