🧠 Mastering useReducer
in React: A Practical Deep Dive
React’s useState
hook is perfect for many scenarios — it’s simple, concise, and expressive. But as your state grows in complexity, managing it with useState
can start to feel messy. That’s where useReducer
shines.
This post will walk you through progressively mastering useReducer
—starting from simple values like counters, all the way to simulating this.setState
behavior, and finally using it for real-world applications like a Tic Tac Toe game.
useReducer
?React’s useReducer
is ideal when:
Let’s explore useReducer
step-by-step with increasingly powerful examples.
useState
to useReducer
— Basic UsageLet’s start by replacing useState
with useReducer
for something simple: updating an input value.
function nameReducer(previousName: string, newName: string) {return newName}const initialNameValue = 'Joe'function NameInput() {const [name, setName] = useReducer(nameReducer, initialNameValue)const handleChange = (e) => setName(e.currentTarget.value)return (<div><label>Name: <input defaultValue={name} onChange={handleChange} /></label><div>You typed: {name}</div></div>)}
👉 In this case, useReducer
behaves just like useState
. The reducer simply returns the new value passed via dispatch
.
Let’s build a counter:
const countReducer = (currentCount: number, action: number) =>currentCount + actionconst initialCount = 0const step = 1function Counter() {const [count, changeCount] = useReducer(countReducer, initialCount)const increment = () => changeCount(step)const decrement = () => changeCount(-step)return (<div><button onClick={decrement}>–</button><span>{count}</span><button onClick={increment}>+</button></div>)}
🧠 Lesson: The action
can be any value — number, string, object, or function — it’s up to you how to use it inside the reducer.
this.setState
)Now let’s work with objects to group state variables together — much like class components used to do with this.setState
.
function countReducer(state, newState) {return { ...state, ...newState }}const initialState = {count: 0,someOtherState: 'hello',}function Counter() {const [state, setState] = useReducer(countReducer, initialState)const { count } = stateconst step = 1const increment = () => setState({ count: count + step })const decrement = () => setState({ count: count - step })return (<div><div>{state.someOtherState}</div><button onClick={decrement}>–</button><span>{count}</span><button onClick={increment}>+</button></div>)}
💡 Why this matters: Updating state via object merging makes useReducer
behave a lot like this.setState
, giving you flexibility while still keeping logic outside your UI code.
this.setState
Simulation)Let’s make our reducer handle both objects and functions, just like this.setState
could.
function countReducer(state, action) {const newState = typeof action === 'function' ? action(state) : actionreturn { ...state, ...newState }}
Now your component can call setState
with a function:
const increment = () =>setState((currentState) => ({ count: currentState.count + step }))
✅ Benefit: This allows state updates based on the previous state — very useful when updates are asynchronous or based on current values.
Let’s follow the conventional reducer pattern with action types. This is common in Redux and large-scale React apps:
function countReducer(state, action) {switch (action.type) {case 'INCREMENT':return { ...state, count: state.count + action.step }case 'DECREMENT':return { ...state, count: state.count - action.step }default:return state}}
Then in your component:
const [state, dispatch] = useReducer(countReducer, { count: 0 })const step = 1const increment = () => dispatch({ type: 'INCREMENT', step })const decrement = () => dispatch({ type: 'DECREMENT', step })
🧘 Now your component just dispatches actions — no logic about how state changes is embedded in the UI.
useReducer
in Tic Tac ToeLet’s go further and use useReducer
in a game.
We’ll define our actions like this:
type GameAction =| { type: 'SELECT_SQUARE'; index: number }| { type: 'SELECT_STEP'; step: number }| { type: 'RESTART' }
And the reducer:
function gameReducer(state, action) {switch (action.type) {case 'SELECT_SQUARE': {const { index } = action// logic to update the boardreturn newState}case 'SELECT_STEP': {return {...state,currentStep: action.step,}}case 'RESTART': {return getInitialGameState()}default:return state}}
Using useReducer
:
const [state, dispatch] = useReducer(gameReducer, null, getInitialGameState)
🌀 Lazy initialization: We pass a third argument to useReducer
(getInitialGameState
) to avoid calculating initial state on every render.
useReducer
is an advanced tool that opens up better patterns for managing complex state. Here’s a quick breakdown of what we’ve learned:
Pattern | Description |
---|---|
Basic value reducer | Like useState , but abstracted |
Object merging like setState | Useful for grouped state values |
Function updater support | Handles async-safe updates |
Action type pattern | Scales for larger apps |
Real-world use (e.g. game) | Clean separation of logic and UI |
useReducer
useState
or useReducer
?useState
with useReducer
Quick Links
Legal Stuff
Social Media