State with hooks

useState and useReducer, batched updates, immutable updates for objects and arrays, and deriving values instead of storing them.

useState and the rules of updates

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(c => c + 1)}>+1 (functional)</button>
      <button onClick={() => setCount(0)}>reset</button>
      <p>{count}</p>
    </>
  )
}
  • State updates are asynchronous and batched: after setCount(count + 1) twice in one handler, count is still the old value in both calls. Use the updater form setCount(c => c + 1).
  • Hooks must be called at the top level of a component or another hook — never inside a condition, a loop or an event handler.
  • Use one useState per independent value. Combining unrelated fields into one object makes updates verbose and re-renders coarse.
  • useState with a function argument (useState(() => expensive())) runs the initialiser once; passing the call result runs it on every render.
// immutable updates: build a new object / array, never mutate the old one
const [form, setForm] = useState({ email: '', name: '' })
setForm(prev => ({ ...prev, email: '[email protected]' }))     // object merge

const [tags, setTags] = useState([])
setTags(prev => [...prev, 'new'])                    // append
setTags(prev => prev.filter(t => t !== 'new'))       // remove
setTags(prev => prev.map(t => (t === 'x' ? 'y' : t)))  // replace

useReducer for related fields

import { useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, items: [...state.items, action.item] }
    case 'remove':
      return { ...state, items: state.items.filter(i => i.id !== action.id) }
    case 'clear':
      return { ...state, items: [] }
    default:
      throw new Error('unknown action: ' + action.type)
  }
}

const [cart, dispatch] = useReducer(reducer, { items: [] })
dispatch({ type: 'add', item: product })
  • Reach for useReducer when several values change together or the next state depends on the previous in more than one way.
  • A reducer must be pure: same input, same output, no fetching, no logging to a server, no mutating its arguments.
  • Throwing on an unknown action turns a silent typo into an immediate error — worth the extra line.
  • Context plus a reducer is the usual lightweight alternative to a state library for medium-sized apps.

Deriving instead of storing

import { useState, useMemo } from 'react'

function ProductTable({ products, query }) {
  const [sort, setSort] = useState('name')

  // derived at render time - no state, nothing to keep in sync
  const visible = useMemo(() => {
    const q = query.toLowerCase()
    return products
      .filter(p => p.name.toLowerCase().includes(q))
      .sort((a, b) => String(a[sort]).localeCompare(String(b[sort])))
  }, [products, query, sort])

  return <Table rows={visible} onSort={setSort} />
}
SituationTool
A value can be computed from props or statePlain expression in the render body
The computation is expensive and inputs rarely changeuseMemo
A stable function identity is needed by a memoised childuseCallback
A value must survive renders without triggering oneuseRef
State changes must be synchronised with something outside ReactuseEffect
⚠️
Storing derived data in state is the most common source of inconsistent UI — two copies that drift apart. If a value can be computed from props and state, compute it during render; add useMemo only when profiling shows the work is actually expensive.

FAQ

Does setState merge objects like the old class API?
No. The hook replaces the value entirely, so spread the previous object yourself: setForm(prev => ({ ...prev, name })).
Is useMemo required for performance?
Rarely. It costs memory and comparison work on every render. Measure, then memoise the specific expensive computation rather than sprinkling it everywhere.

Components and props Effects and data fetching

Last refreshed 2026-09-18.