HomeAbout Me

Advanced React Patterns 7: State Reducer

By Daniel Nguyen
Published in React JS
June 27, 2025
1 min read
Advanced React Patterns 7: State Reducer

State Reducer

**One liner:** The State Reducer Pattern inverts control over the state management of your hook and/or component to the developer using it so they can control the state changes that happen when dispatching events.

During the life of a reusable component which is used in many different contexts, feature requests are made over and over again to handle different cases and cater to different scenarios.

We could definitely add props to our component and add logic in our reducer for how to handle these different cases, but there’s a never ending list of logical customizations that people could want out of our custom hook and we don’t want to have to code for every one of those cases.

For example, imagine you’ve got a combobox component and by default when the user clicks out of the combobox, the menu closes. But then someone wants to prevent the menu from closing when the user clicks out of the combobox.

You can use the state reducer pattern to allow the user of your hook to control the state changes that happen when events are dispatched. Here’s an example of what that may look like on the ComboBox component:

<ComboBox
stateReducer={(state, action) => {
if (action.type === 'clickOutside' && state.isOpen) {
return { ...state, isOpen: false }
}
return state
}}
// ...other props
/>

📜 Read more about this pattern in: The State Reducer Pattern with React Hooks

Real World Projects that use this pattern:

  • downshift

State Reducer

👨‍💼 In this exercise, we want to prevent the toggle from updating the toggle state after it’s been clicked 4 times in a row before resetting. We could easily add that logic to our reducer, but instead we’re going to apply a computer science pattern called “Inversion of Control” where we effectively say: “Here you go! You have complete control over how this thing works. It’s now your responsibility.”

Your job is to enable people to provide a custom reducer so they can have complete control over how state updates happen in our <Toggle /> component.

As an aside, before React Hooks were a thing, this was pretty tricky to implement and resulted in pretty weird code, but with useReducer, this is WAY better. I ❤️ hooks. 😍# Default State Reducer

👨‍💼 Our toggleReducer is pretty simple, so it’s not a huge pain for people to implement their own. However, in a more realistic scenario, people may struggle with having to basically re-implement our entire reducer which could be pretty complex.

So for the this step, we’re going to export the default reducer so people can use that inside their own reducers as needed.

Go ahead and do this by changing the toggleStateReducer function inside the <App /> example to use the default reducer instead of having to re-implement what to do when the action type is 'reset'.

app.tsx

import { useState } from 'react'
import { Switch } from '#shared/switch.tsx'
import { toggleReducer, useToggle } from './toggle.tsx'
export function App() {
const [timesClicked, setTimesClicked] = useState(0)
const clickedTooMuch = timesClicked >= 4
const { on, getTogglerProps, getResetterProps } = useToggle({
reducer(state, action) {
if (action.type === 'toggle' && clickedTooMuch) {
return state
}
return toggleReducer(state, action)
},
})
return (
<div>
<Switch
{...getTogglerProps({
on: on,
onClick: () => setTimesClicked(count => count + 1),
})}
/>
{clickedTooMuch ? (
<div data-testid="notice">
Whoa, you clicked too much!
<br />
</div>
) : timesClicked > 0 ? (
<div data-testid="click-count">Click count: {timesClicked}</div>
) : null}
<button {...getResetterProps({ onClick: () => setTimesClicked(0) })}>
Reset
</button>
</div>
)
}

toggle.tsx

import { useReducer, useRef } from 'react'
function callAll<Args extends Array<unknown>>(
...fns: Array<((...args: Args) => unknown) | undefined>
) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}
type ToggleState = { on: boolean }
type ToggleAction =
| { type: 'toggle' }
| { type: 'reset'; initialState: ToggleState }
export function toggleReducer(state: ToggleState, action: ToggleAction) {
switch (action.type) {
case 'toggle': {
return { on: !state.on }
}
case 'reset': {
return action.initialState
}
}
}
export function useToggle({ initialOn = false, reducer = toggleReducer } = {}) {
const { current: initialState } = useRef<ToggleState>({ on: initialOn })
const [state, dispatch] = useReducer(reducer, initialState)
const { on } = state
const toggle = () => dispatch({ type: 'toggle' })
const reset = () => dispatch({ type: 'reset', initialState })
function getTogglerProps<Props>({
onClick,
...props
}: { onClick?: React.ComponentProps<'button'>['onClick'] } & Props) {
return {
'aria-checked': on,
onClick: callAll(onClick, toggle),
...props,
}
}
function getResetterProps<Props>({
onClick,
...props
}: { onClick?: React.ComponentProps<'button'>['onClick'] } & Props) {
return {
onClick: callAll(onClick, reset),
...props,
}
}
return {
on,
reset,
toggle,
getTogglerProps,
getResetterProps,
}
}

Tags

#React

Share

Previous Article
Advanced React Patterns 6: State Initializer

Table Of Contents

1
State Reducer
2
State Reducer

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media