useState & State Updates

Easy

useState is the fundamental hook for adding local state to functional components. It returns a state value and a setter function. State updates are asynchronous and batched, and using the updater function form ensures correct updates when the new value depends on the previous one.

Interactive Visualization

Key Points

  • Returns a [value, setter] tuple, initialized with the argument
  • Setter triggers a re-render with the new value
  • Use updater function form when new state depends on previous: setValue(prev => prev + 1)
  • State updates are batched in React 18+ for all event types
  • Lazy initialization accepts a function to avoid expensive computation on every render
  • State identity check uses Object.is() — same value skips re-render
  • Objects and arrays must be replaced, not mutated, to trigger re-renders

Code Examples

Counter with Updater Function

import { useState } from 'react'

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

  const increment = () => {
    // Updater form: correct when called multiple times
    setCount((prev) => prev + 1)
  }

  const incrementThrice = () => {
    // All three updates use the latest value
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
    // count will increase by 3
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={incrementThrice}>+3</button>
    </div>
  )
}

The updater function receives the latest pending state, ensuring correct results when multiple updates are batched

Object State with Immutable Updates

import { useState } from 'react'

interface FormData {
  name: string
  email: string
  age: number
}

function ProfileForm() {
  const [form, setForm] = useState<FormData>({
    name: '',
    email: '',
    age: 0,
  })

  const updateField = <K extends keyof FormData>(
    field: K,
    value: FormData[K]
  ) => {
    setForm((prev) => ({ ...prev, [field]: value }))
  }

  return (
    <form>
      <input
        value={form.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        value={form.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
    </form>
  )
}

Object state must be replaced with a new object using spread syntax, never mutated directly

Lazy Initialization

import { useState } from 'react'

function ExpensiveComponent() {
  // Function is only called on initial render
  const [data, setData] = useState(() => {
    const stored = localStorage.getItem('app-data')
    return stored ? JSON.parse(stored) as Record<string, unknown> : {}
  })

  return <div>{JSON.stringify(data)}</div>
}

Passing a function to useState defers expensive computation to the first render only, avoiding it on every re-render

Common Mistakes

  • Mutating state objects directly instead of creating new references
  • Using setCount(count + 1) inside loops or async code instead of the updater function
  • Expecting state to update synchronously right after calling the setter

Interview Tips

  • Explain batching: React 18 batches all state updates, not just event handlers
  • Know why Object.is() comparison means you must create new references for objects
  • Discuss when to use useState vs useReducer for complex state

Related Concepts