Topic 10: Introduction to Redux
📖 12 min read · 🎯 intermediate · 🧭 Prerequisites: react-router, hooks
Why this matters
Up until now, you've been passing data between React components using props — and for small apps, that works fine. But as your app grows, you end up passing the same data through five or six components just to get it where it's needed. Change one thing, and you're not sure what broke or why. I've seen beginners spend hours debugging state that's spread across dozens of components with no clear owner. Redux solves this by giving your entire app a single place to store state — one store, clear rules for updating it, and a predictable flow you can actually trace and trust.
What You'll Learn
- Understand Redux's three core principles and why they lead to predictable state
- Create a Redux store, define action types, write action creators, and build reducers
- Combine multiple reducers with
combineReducers - Connect a Redux store to a React application using the
Providercomponent - Read state and dispatch actions inside components with
useSelectoranduseDispatch
The Analogy
Think of Redux as Vizag's official city ledger. Every transaction that changes the city's finances must be submitted as a written request (an action) to the city clerk (the reducer), who applies it to the single master ledger (the store) and returns an updated copy — the original is never crossed out. No one can sneak in and change the balance directly; every change is recorded, auditable, and reproducible from the very first entry. Any department (component) that needs the current balance just reads from the official ledger. When the ledger changes, every department is automatically notified.
Chapter 1: What is Redux?
Redux is a predictable state container for JavaScript applications. It is framework-agnostic but pairs especially well with React via the react-redux binding library. Redux's design makes applications that behave consistently across environments and are straightforward to test.
Redux is built on three core principles:
- Single Source of Truth — The entire application state lives in one object tree inside a single store. There is no scattered local state for data that multiple components share.
- State is Read-Only — The only way to change state is to dispatch an action: a plain object that describes what happened. Nothing mutates state directly.
- Changes are Made with Pure Functions — Reducers are pure functions that take the current state and an action, then return the next state without mutating the original.
flowchart LR
UI["React Component"] -->|"dispatch(action)"| Store
Store -->|"calls"| Reducer
Reducer -->|"returns new state"| Store
Store -->|"notifies via useSelector"| UI
Chapter 2: Setting Up a Redux Environment
The class scaffolded a new React project with Vite (Create React App was officially sunset by the React team in 2025 — the React docs now point to Vite, Next.js, or Remix) and installed the Redux packages.
npm create vite@latest redux-intro -- --template react
cd redux-intro
npm install
npm install @reduxjs/toolkit react-redux
Modern Redux note: This lesson teaches classic Redux (hand-rolled action types, action creators, and reducer functions) so you can read existing codebases. In any new app you should reach for Redux Toolkit (
@reduxjs/toolkit) — it bundles the officialconfigureStoreandcreateSlicehelpers that replace the boilerplate below with about 80% less code. See the Redux Toolkit Quick Start to migrate this example once you've internalised the core concepts.
@reduxjs/toolkit re-exports the core Redux API (legacy_createStore, combineReducers) and adds the modern configureStore/createSlice helpers — so we never need to install the bare redux package directly. react-redux provides Provider, useSelector, and useDispatch — the bridge between Redux and React.
Chapter 3: The Redux Store
The store is the single object that holds all application state and wires together actions and reducers. You create it once by calling createStore with the root reducer.
src/store.js
import { legacy_createStore as createStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
rootReducer is imported from the reducers/ directory (assembled in Chapter 5). The store exposes three key methods: getState(), dispatch(action), and subscribe(listener).
Chapter 4: Actions
Actions are plain JavaScript objects that describe what happened. They are the only way to signal to Redux that state should change. Every action must have a type property; it can carry any additional data needed.
Defining Action Types
Defining types as named constants prevents typos from silently failing and makes the intent searchable across the codebase.
src/actions/actionTypes.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
Creating Action Creators
Action creators are functions that return action objects. Centralising them means components never hand-craft raw objects.
src/actions/index.js
import { INCREMENT, DECREMENT } from './actionTypes';
export const increment = () => {
return {
type: INCREMENT
};
};
export const decrement = () => {
return {
type: DECREMENT
};
};
Chapter 5: Reducers
Reducers specify how state changes in response to an action. A reducer is a pure function: same inputs always produce the same output, and it never mutates the state argument directly — it spreads the existing state and overwrites only what changed.
Creating a 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 details: the default case returns the existing state unchanged (required), and spread syntax (...state) copies all existing keys before overwriting count.
Combining Reducers
Real applications have many slices of state. combineReducers merges multiple reducer functions into one root reducer, each managing its own slice.
src/reducers/index.js
import { combineReducers } from '@reduxjs/toolkit';
import counterReducer from './counterReducer';
const rootReducer = combineReducers({
counter: counterReducer
});
export default rootReducer;
The key you pass to combineReducers (counter) is the key your components will use to access that slice of state: state.counter.count.
Chapter 6: Connecting Redux to React
The Provider component from react-redux makes the Redux store available to every component in the tree. Wrap the entire <App /> with it at the entry point.
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, so reaching for createRoot is the only path that works on modern React.
Without Provider, any call to useSelector or useDispatch inside the component tree will throw an error because it cannot locate the store.
Chapter 7: Using Redux in React Components
With the store wired up, components interact with Redux through two hooks from react-redux:
useSelector(selector)— subscribes to the store and returns whatever the selector extracts. Re-renders the component when that slice changes.useDispatch()— returns the store'sdispatchfunction so the component can send actions.
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.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;
state.counter.count matches the shape set by combineReducers — the counter key maps to counterReducer, and count is the field inside its initialState.
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;
🧪 Try It Yourself
Task: Extend the counter with a "Reset" feature.
- Add a
RESETconstant tosrc/actions/actionTypes.js. - Add a
resetaction creator tosrc/actions/index.jsthat returns{ type: RESET }. - Handle
RESETincounterReducer.jsby returning{ ...state, count: 0 }. - Add a "Reset" button to
Counter.jsthat dispatchesreset().
Success criterion: Clicking Increment a few times then clicking Reset should display Count: 0 in the browser. Open React DevTools Redux tab (if you have the Redux DevTools browser extension installed) and confirm the action history shows RESET when you click the button.
Starter snippet for the new action creator:
import { INCREMENT, DECREMENT, RESET } from './actionTypes';
export const reset = () => {
return {
type: RESET
};
};
🔍 Checkpoint Quiz
Q1. Which of Redux's three core principles states that you can never modify state directly?
A) Single Source of Truth
B) State is Read-Only
C) Changes are Made with Pure Functions
D) Reducers Must Be Synchronous
Q2. Given the following reducer, what does store.getState().counter.count equal after dispatching increment() twice and then decrement() once, starting from initialState?
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 the purpose of combineReducers in a Redux application?
A) It merges multiple stores into one global store
B) It composes multiple reducer functions so each manages its own state slice under a named key
C) It combines all action creators into a single dispatch call
D) It automatically syncs Redux state to localStorage
Q4. A CounterDisplay component needs to read state.counter.count and dispatch the increment() action creator. Which pair of hooks should it use?
A) useState and useEffect
B) useContext and useReducer
C) useSelector and useDispatch
D) useStore and useAction
A1. B — "State is Read-Only" means the only mechanism for change is dispatching an action; direct mutation is never allowed.
A2. B — Starting at 0, two increments bring it to 2, one decrement brings it back to 1.
A3. B — combineReducers takes an object of reducer functions and returns a single root reducer where each key corresponds to one reducer's slice of state. There is still only one store.
A4. C — useSelector(state => state.counter.count) reads the slice; useDispatch() returns the dispatch function so the component can fire dispatch(increment()). useStore exists but is rarely needed in normal components, and useAction is not a React-Redux API.
🪞 Recap
- Redux is a predictable state container built on three principles: single source of truth, read-only state, and pure reducer functions.
- You install two packages:
@reduxjs/toolkit(which re-exports the core Redux API and bundles modern helpers) andreact-reduxfor the React integration hooks. - Actions are plain objects with a
type; action creators are functions that return those objects; reducers handle each type and return new state without mutation. combineReducersmerges multiple reducers into a root reducer, partitioning state into named slices.Providermakes the store available to the entire React tree;useSelectorreads state anduseDispatchsends actions from any component.
📚 Further Reading
- Redux official docs — the authoritative guide to core concepts, middleware, and advanced patterns
- React-Redux docs — covers
Provider,useSelector,useDispatch, and the full hooks API - Redux Toolkit — the officially recommended way to write Redux today, reducing boilerplate significantly
- ⬅️ Previous: Hooks
- ➡️ Next: Redux Toolkit Quick Start (external — the recommended next step for any new Redux app)