React Hooks Part 3: useRef and useReducer
useRef gives you a mutable container that survives renders without causing them. useReducer brings Redux-style state machines to any component. Together they handle the cases useState and useEffect cannot.
React Hooks: A Complete Guide
A four-part deep-dive into React Hooks — from managing state with useState to squeezing performance out of useCallback and useMemo. Each article is standalone yet builds on the previous, giving you both quick reference and progressive mastery.
- 1 React Hooks Part 1: Managing State with useState
- 2 React Hooks Part 2: Side Effects with useEffect
- 3 React Hooks Part 3: useRef and useReducer
- 4 React Hooks Part 4: useCallback, useMemo, and Custom Hooks
- 5 React Hooks Part 5: Sharing State with useContext
Table of Contents
useRef: a box React doesn't watch
useRef returns a plain object with a single .current property. Updating .current does not trigger a re-render — React doesn't know or care when it changes.
import { useRef } from 'react'
const ref = useRef(initialValue)
// ref.current === initialValue
Two primary use-cases
1. Accessing a DOM node
function SearchBar() {
const inputRef = useRef(null)
const focusInput = () => inputRef.current?.focus()
return (
<>
<input ref={inputRef} type="text" placeholder="Search…" />
<button onClick={focusInput}>Focus</button>
</>
)
}
2. Storing a mutable value across renders
function Stopwatch() {
const [elapsed, setElapsed] = useState(0)
const intervalRef = useRef(null)
const start = () => {
intervalRef.current = setInterval(
() => setElapsed(prev => prev + 1),
1000
)
}
const stop = () => clearInterval(intervalRef.current)
return (
<div>
<p>{elapsed}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
Storing the interval ID in state would cause an unnecessary re-render on start/stop.
useState vs useRef
| useState | useRef | |
|---|---|---|
| Triggers re-render | Yes | No |
| Update is async | Yes | No |
| Holds DOM node | No | Yes |
| Survives renders | Yes | Yes |
useReducer: predictable state machines
useReducer is an alternative to useState for state that involves multiple sub-values or transitions that depend on the current state.
const [state, dispatch] = useReducer(reducer, initialState)
A reducer is a pure function: given the current state and an action, it returns the next state.
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 }
case 'DECREMENT': return { count: state.count - 1 }
case 'RESET': return { count: 0 }
default: return state
}
}
Counter with useReducer
import { useReducer } from 'react'
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>−1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
)
}
Form with useReducer
The real power shows with forms — where useState would require one setter per field:
const initialForm = { name: '', email: '', password: '' }
function formReducer(state, action) {
switch (action.type) {
case 'FIELD': return { ...state, [action.field]: action.value }
case 'RESET': return initialForm
default: return state
}
}
function SignupForm() {
const [form, dispatch] = useReducer(formReducer, initialForm)
const field = name => ({
value: form[name],
onChange: e => dispatch({ type: 'FIELD', field: name, value: e.target.value }),
})
return (
<form>
<input {...field('name')} placeholder="Name" />
<input {...field('email')} type="email" placeholder="Email" />
<input {...field('password')} type="password" placeholder="Password" />
<button type="button" onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</form>
)
}
When to prefer useReducer over useState
- State is an object with 3+ fields that change together.
- Next state depends on multiple parts of the current state.
- You want the transition logic to be testable in isolation.
- You plan to share dispatch via context across deep component trees.
Key takeaways
- useRef — mutable container that doesn't re-render; use for DOM refs and values that shouldn't cause updates.
- useReducer — centralise complex state transitions in a pure function; makes logic easy to test.
Next up: useCallback, useMemo & Custom Hooks — performance optimisation and code reuse.