Reducers, Store, and Dispatch: Building Your First Redux App
Redux in code: writing your first reducer, creating a store with createStore, and dispatching actions to see state change in real time.
The data flow cycle is clear in theory. Five stages, one direction, always. Now let's make it run.
This post builds a working Redux app from scratch. No React component yet. Just pure Redux to see exactly how the store, reducer, and dispatch fit together.
[!TIP] Run this yourself: The code for this post is in the code-practice repo. Clone it,
npm install, then runnode 01-redux-core/score-tracker.js(quiz) ornode 01-redux-core/bank.js(deposit/withdraw).
Setup
Start with a plain React project and add Redux:
npx create-react-app quiz-app
cd quiz-app
npm install redux@4The State and the Reducer
Redux needs to know two things upfront: what the initial state looks like, and what to do when an action arrives. Both live in the reducer.
Here is a quiz score tracker with two operations: award points on a correct answer, subtract on a wrong one.
// index.js
const defaultState = 0
function scoreReducer(state = defaultState, action) {
switch (action.type) {
case 'ANSWER_CORRECT':
return state + 10
case 'ANSWER_WRONG':
return state - 5
default:
return state
}
}Walk through what this does:
- The function takes two arguments:
stateandaction. Both are provided automatically by the store. You never call this function directly. state = defaultStatehandles the first call. The first time Redux invokes the reducer, it passesundefinedas the state. The default parameter catches that and substitutes0.- The
switchchecksaction.typeand returns a new state value for each known operation. - The
defaultcase is not optional. If an unknown action arrives, the reducer must return the current state unchanged. Omitting this breaks the Redux lifecycle.
The return value is the new state. Whatever scoreReducer returns is stored in Redux and becomes the state argument the next time the reducer is called.
Creating the Store
import { createStore } from 'redux'
const store = createStore(scoreReducer)createStore takes the reducer and returns a store object. The moment the store is created, it invokes the reducer once automatically with a special @@redux/INIT action. That initialisation call sets the initial state. This is how defaultState = 0 becomes the starting value.
Read the current state at any time with getState:
console.log(store.getState()) // 0Dispatching Actions
dispatch is how you trigger a state change. Pass it an action object with a type field:
store.dispatch({ type: 'ANSWER_CORRECT' })
console.log(store.getState()) // 10
store.dispatch({ type: 'ANSWER_CORRECT' })
console.log(store.getState()) // 20
store.dispatch({ type: 'ANSWER_WRONG' })
console.log(store.getState()) // 15Each dispatch call runs the reducer with the current state and the action you provided. The reducer returns the new state. The store stores it. getState() reflects the update.
ExpandThe reducer as a pure function, transforming state + action into new state
If you dispatch an action type that does not exist in the reducer, nothing happens:
store.dispatch({ type: 'SOMETHING_ELSE' })
console.log(store.getState()) // 15, unchangedThe default case returns the current state, so the score stays at 15.
The One Rule You Cannot Break
Never mutate state directly inside a reducer.
Do this wrong:
// Wrong: mutates state in place
case 'ADD_TAG':
state.tags.push(action.payload)
return stateDo this right:
// Correct: returns a new array
case 'ADD_TAG':
return { ...state, tags: [...state.tags, action.payload] }The reason is not just stylistic. Redux compares state references to decide whether components need to re-render. If you mutate the existing object and return it, the reference stays the same. Redux sees "no change" and skips the re-render, even though the data is different.
Always return a new object or array. Spread operators and array methods like map, filter, and concat are your tools.
What the Store Lifecycle Looks Like
Every call to dispatch follows the same sequence:
- The store receives the action
- The store calls
scoreReducer(currentState, action) - The reducer returns a new value
- The store replaces the current state with that value
- Any subscribed components are notified
This is the same five-stage cycle every single time. The store is the coordinator. The reducer is the logic. dispatch is the trigger.
The next piece is wiring this into an actual React component so users can dispatch actions by clicking buttons, and the UI updates automatically. But first, there's one tool you need before that: Redux DevTools.
The Essentials
- The reducer is a function with signature
(state = defaultState, action) => newState. The store calls it automatically. You never invoke it directly. createStore(reducer)initialises the Redux store. It immediately calls the reducer once to set the starting state.store.dispatch({ type: 'ACTION_TYPE' })is how you trigger state changes. The reducer receives the action, computes new state, and the store stores it. Never mutate state in place.
Further Reading and Watching
- Redux Fundamentals, Part 3: State, Actions, and Reducers: official walkthrough of reducer implementation
- Redux Fundamentals, Part 4: Store: createStore, dispatch, and getState in depth
Practice what you just read.
Keep reading