TUTORIAL

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.


SERIES

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.

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.

JSX
import { useRef } from 'react'

const ref = useRef(initialValue)
// ref.current === initialValue

Two primary use-cases

1. Accessing a DOM node

JSX
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

JSX
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.

JSX
const [state, dispatch] = useReducer(reducer, initialState)

A reducer is a pure function: given the current state and an action, it returns the next state.

JSX
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

JSX
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:

JSX
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.


Was this article helpful?

w

webencher Editorial

Software engineers and technical writers with 10+ years of combined experience in algorithms, systems design, and web development. Every article is reviewed for accuracy, depth, and practical applicability.

More by this author →