🔁 The State Initializer Pattern in React: Resetting with Confidence
One of the fundamental building blocks of React is useState()
. But sometimes, initializing state is only half the story—you also want a way to reset it.
The problem? Without proper care, that reset may not behave how you think it should.
Let’s dig into the State Initializer pattern, how to implement it correctly, and where it can go wrong. 🧠
Let’s start simple with a useCounter
hook:
function useCounter() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return { count, increment }}
Nothing special. It just counts. But what if we want to let users set the starting value?
We can allow for configuration like this:
function useCounter({ initialCount = 0 } = {}) {const [count, setCount] = useState(initialCount)const increment = () => setCount(c => c + 1)return { count, increment }}
Now you can start your counter at 10
, 50
, or any value:
const { count } = useCounter({ initialCount: 10 })
Great! But now comes the kicker…
We want to let users reset the count back to that initial value. That means we need to keep track of the original value:
function useCounter({ initialCount = 0 } = {}) {const [count, setCount] = useState(initialCount)const increment = () => setCount(c => c + 1)const reset = () => setCount(initialCount)return { count, increment, reset }}
So what’s wrong with that?
Let’s say a parent component re-renders and passes a new initialCount
value. Your hook will use the new value for resets—not the original one. 😬
That’s not what most people expect. The term “reset” implies going back to the state at the time the component was initialized—not the current value of the prop.
The solution? Use a ref
to capture the initial value once—and never let it change:
function useCounter({ initialCount = 0 } = {}) {const initialCountRef = useRef(initialCount)const [count, setCount] = useState(initialCountRef.current)const increment = () => setCount(c => c + 1)const reset = () => setCount(initialCountRef.current)return { count, increment, reset }}
🧠 useRef
stores a value that persists across renders without causing re-renders. It’s perfect for capturing “once-on-mount” values like this.
This pattern gets even more important when you manage state with a reducer
. Here’s a simplified useToggle
hook:
function toggleReducer(state, action) {switch (action.type) {case 'toggle': return { ...state, on: !state.on }case 'reset': return { ...state, on: action.initialOn }default: throw new Error('Unhandled action type')}}
And here’s the hook implementation:
function useToggle({ initialOn = false } = {}) {const initialOnRef = useRef(initialOn)const [state, dispatch] = useReducer(toggleReducer, {on: initialOnRef.current,})const toggle = () => dispatch({ type: 'toggle' })const reset = () => dispatch({ type: 'reset', initialOn: initialOnRef.current })return { on: state.on, toggle, reset }}
Now no matter how initialOn
changes later from a parent component, your reset behavior is locked in to the original value passed.
Let’s say we do this:
function Parent() {const [isDarkMode, setDarkMode] = useState(true)return (<><button onClick={() => setDarkMode(prev => !prev)}>Toggle Initial</button><Toggle initialOn={isDarkMode} /></>)}
Now if the user toggles isDarkMode
and then hits reset in the Toggle
component…
useRef
: Reset will use the current isDarkMode
, not the original.useRef
: Reset will go back to the original isDarkMode
value.This difference is subtle but can lead to serious bugs—especially when components are reused or toggled conditionally.
Do | Don’t |
---|---|
Use useRef to lock in initial state | Don’t trust current initialX values after mount |
Combine with useReducer for complex state logic | Avoid repeating the same initialX logic in multiple places |
Name your initializer and reset methods clearly | Don’t rely on useEffect to “patch” initialization |
Use this pattern whenever your component:
Some examples:
The State Initializer Pattern is simple but powerful. By using useRef
, you can avoid subtle bugs and make your components more predictable—especially in dynamic or complex applications.
Reset behavior should feel intuitive. By anchoring to the original initial value, you ensure your users (and teammates) always know what to expect.
Want a CodeSandbox demo of this pattern in action? Just ask and I’ll hook you up with a live example! 🧑🔧
Happy resetting! 💡
Quick Links
Legal Stuff
Social Media