Topic 10: Introduction to Redux
📖 6 min read · 🎯 Advanced · 🧭 Prerequisites: socket-io-the-front-end-and-a-chat-app, using-other-open-source-material
Why this matters
Up until now, you've been passing state down through props — parent to child, child to grandchild, maybe even deeper. It works fine for small apps. But the moment your app grows, that approach starts to crack. You end up chasing a piece of data across five components just to change one value, and debugging becomes a nightmare. Redux solves this by giving your entire app a single place where state lives — one store, one source of truth. Every component reads from it, every update goes through it. No more prop drilling, no more guessing where the data came from.
What You'll Learn
- Understand Redux's three core principles and why they make state predictable
- Install
reduxandreact-reduxand scaffold a Redux project - Create a Redux store, define action types, and write action creators
- Write and combine reducers to handle state transitions
- Connect the Redux store to React components using
Provider,useSelector, anduseDispatch
The Analogy
Think of Redux like a city-hall ledger system. Every department in Vizag — parks, roads, treasury — wants to update the city's records. Without rules, anyone can scribble directly in the ledger and it becomes chaos. Redux enforces a strict protocol: every change must be submitted as a formal action slip (an action object) to the clerk (the reducer), who then writes the approved update into the master ledger (the store). No one touches the ledger directly. The clerk never makes a decision without a slip. And there is exactly one ledger — not one per department. The result: a complete, auditable, predictable history of every change that ever happened.
Chapter 1: What Is Redux?
Redux is a predictable state container for JavaScript applications. While it works with any JS framework, it is particularly popular with React. Its power comes from three ironclad core principles.
Core Principles of Redux
- Single Source of Truth — The entire application state lives in one object tree inside a single store. One ledger, city-wide.
- State is Read-Only — The only way to change state is to emit an action: a plain object that describes what happened. You cannot mutate the store directly.
- Changes are Made with Pure Functions — Reducers are pure functions that take the current state and an action, then return the next state. Same input always produces the same output, with no side effects.
These principles make Redux applications consistent across environments (client, server, native), straightforward to test, and trivially debuggable — you can replay every action and reconstruct any past state.
Chapter 2: Setting Up a Redux Environment
The class bootstrapped a fresh React project and added both the core Redux library and the React bindings.
Installing Redux and React-Redux
npx create-react-app redux-intro
cd redux-intro
npm install redux react-redux
redux— the core state container, framework-agnosticreact-redux— the official React bindings; providesProvider,useSelector, anduseDispatch
Chapter 3: The Redux Store
The store is the single object that wires actions and reducers together. It holds application state, allows access via getState(), allows updates via dispatch(action), and registers listeners via subscribe(listener).
Creating the Redux Store
the trainer had the class create src/store.js first — the dispatch tower:
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
createStore takes a root reducer (and optionally middleware enhancers). Everything else flows from this single object.
Chapter 4: Actions
Actions are plain JavaScript objects — the formal "action slips" — that carry information from the application to the store. Every action must have a type property that describes what happened.
Defining Action Types
Storing action type strings as named constants prevents typos and makes refactoring safe.
src/actions/actionTypes.js:
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
Creating Action Creators
Action creators are plain functions that return action objects. They centralise action construction so components don't build raw objects themselves.
src/actions/index.js:
import { INCREMENT, DECREMENT } from './actionTypes';
export const increment = () => {
return {
type: INCREMENT
};
};
export const decrement = () => {
return {
type: DECREMENT
};
};
Keeping action creators in one place means every component dispatches the same shape — no drift, no "slightly different" objects scattered across the app.
Chapter 5: Reducers
Reducers are the clerks. They are pure functions with the signature (state = initialState, action) => nextState. They must not mutate the existing state — they return a new object.
Creating a Counter Reducer
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:
state = initialState— the default parameter ensures the reducer always has a valid starting state....statespread — we return a new object rather than mutating the existing one, satisfying the pure-function rule.- The
defaultcase returns the existing state unchanged for any unknown action type.
Combining Reducers
Real applications have many slices of state — auth, UI, data, etc. combineReducers merges them into the single root reducer that createStore expects.
src/reducers/index.js:
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
const rootReducer = combineReducers({
counter: counterReducer
});
export default rootReducer;
The key counter here becomes the namespace in state: state.counter.count. Add future reducers as additional keys.
flowchart LR
A([Component]) -->|dispatch action| B[Redux Store]
B --> C{Root Reducer}
C --> D[counterReducer]
C --> E[otherReducer...]
D -->|new state slice| B
B -->|state update| A
Chapter 6: Connecting Redux to React
Redux doesn't know about React — react-redux is the bridge. The Provider component makes the store available to every nested component without prop-drilling.
Wrapping the App with Provider
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')
);
Provider accepts the store as a prop and uses React context internally to make it accessible anywhere in the tree. This is a one-time setup — you never pass the store as a prop to individual components.
Chapter 7: Using Redux in React Components
With the store provided at the root, any component can read state and dispatch actions using two hooks from react-redux.
| Hook | Purpose |
|---|---|
useSelector(selector) | Reads a slice of state; re-renders 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.counter.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.counter.count)— the selector function receives the full Redux state and returns only the value this component cares about. The component re-renders only when that specific value changes.dispatch(increment())— calls the action creator, then dispatches the resulting action object to the store. The reducer processes it, state updates, anduseSelectortriggers a re-render.
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;
The final file structure the class built:
src/
├── actions/
│ ├── actionTypes.js
│ └── index.js
├── reducers/
│ ├── counterReducer.js
│ └── index.js
├── components/
│ └── Counter.js
├── store.js
├── App.js
└── index.js
🧪 Try It Yourself
Task: Extend the counter to support 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 RESET:branch incounterReducer.jsthat returns{ ...state, count: 0 }. - Add a Reset button in
Counter.jsthat dispatchesreset().
Success criterion: Clicking Increment a few times then clicking Reset should display Count: 0 in the browser immediately, with no page reload.
Starter snippet for the reducer case:
case RESET:
return {
...state,
count: 0
};
🔍 Checkpoint Quiz
Q1. Which of Redux's core principles means you cannot call store.state.count = 5 directly?
- A) Single Source of Truth
- B) State is Read-Only
- C) Changes are Made with Pure Functions
- D) Immutable Reducers
Q2. Given the following reducer, what does state.counter.count equal after dispatching { type: 'INCREMENT' } three times starting from the initial state?
const initialState = { count: 0 };
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
- A) 0
- B) 1
- C) 3
- D) undefined
Q3. What is the role of combineReducers in a Redux application?
- A) It merges multiple Redux stores into one
- B) It combines multiple action creators into a single function
- C) It merges multiple reducer functions into a single root reducer keyed by namespace
- D) It replaces
createStorewhen you have more than one component
Q4. Your team adds a new feature that needs its own state slice. How would you add a userReducer to the existing rootReducer without breaking the counter?
A1. B — State is Read-Only. The only permitted way to trigger a change is to dispatch an action object; direct mutation is forbidden by this principle.
A2. C — 3. Each INCREMENT dispatch returns a new state object with count + 1, so after three dispatches count equals 3.
A3. C — combineReducers takes an object whose values are individual reducer functions and returns a single root reducer. Each key becomes a namespace on the global state tree (e.g., state.counter, state.user).
A4. Import userReducer in src/reducers/index.js and add it as a new key to the object passed to combineReducers:
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer
});
The counter slice is completely unaffected because reducers are isolated by their key.
🪞 Recap
- Redux enforces three principles — single store, read-only state, pure-function reducers — to make state changes predictable and auditable.
- Actions are plain objects with a
typefield; action creators are factory functions that produce them. - Reducers are pure functions that compute the next state from the current state and an action, using spread syntax to avoid mutation.
combineReducersmerges multiple reducers into a namespaced root reducer thatcreateStoreconsumes.Providermakes the store available tree-wide;useSelectorreads state slices;useDispatchtriggers state changes from any component.
📚 Further Reading
- Redux Official Docs — the source of truth on the full Redux API, middleware, and Redux Toolkit
- React-Redux Hooks API — deep reference for
useSelector,useDispatch, anduseStore - Redux Toolkit — the modern, opinionated Redux setup that reduces boilerplate significantly; recommended for new projects
- ⬅️ Previous: Using Other Open Source Material
- ➡️ Next: Libraries: Image Picker & React Elements