All guides

LEARN \u00b7 DEBUGGING GUIDE

Hidden state bug in React form: how to debug stale closures and state issues

You type in a form field. You hit submit. The submitted value is what was there five keystrokes ago. Or a button's onClick handler uses a state variable that is always one render behind.

IntermediateJavaScript/Node runtime debugging

What this usually means

React closures capture values at the time the closure was created. If a callback (onClick, onSubmit) is defined during one render but called during a later render, it sees the state values from the render when it was defined — not the current values. This is the 'stale closure' problem. It happens most often with setTimeout, setInterval, event handlers passed as props, and callbacks stored in refs or external stores.

( 01 )Fast diagnosis

The first ten minutes \u2014 establish facts before touching code.

  • 1Check if the callback is using state directly vs via a ref or functional update. `setCount(count + 1)` uses a potentially stale count. `setCount(prev => prev + 1)` always uses the latest.
  • 2Check if the callback is wrapped in useCallback with an empty or incomplete dependency array.
  • 3Check if the value comes from a ref. Refs do not trigger re-renders when updated.
  • 4Check if the form is using controlled inputs (value + onChange) not just defaultValue.
  • 5Log the state value inside the callback and outside it. If they differ, you have a stale closure.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe callback or event handler — what state variables does it close over?
  • searchuseCallback and useMemo dependency arrays — are all dependencies listed?
  • searchRefs vs state — is the value in a ref (useRef) instead of state (useState)?
  • searchControlled vs uncontrolled inputs — is the input using value or defaultValue?
  • searchuseEffect cleanup — are timers or subscriptions properly cleaned up?
  • searchState update batching — multiple setState calls in the same event handler are batched
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningCallback closes over a state value from a previous render because the dependency array is empty or incomplete
  • warningUsing `setState(value)` instead of `setState(prev => newValue)` when the new value depends on the old
  • warningForm input uses defaultValue instead of value + onChange (uncontrolled vs controlled)
  • warningTimer or event listener callback was set once and never updated with new state
  • warningState is stored in a ref for performance but never triggers an update when it changes
  • warningMultiple state updates in an async callback are batched unexpectedly in React 18
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildUse functional updates when the new state depends on the old: `setCount(prev => prev + 1)`
  • buildInclude all referenced state and props in useCallback and useMemo dependency arrays
  • buildUse controlled inputs: set `value={state}` and `onChange={(e) => setState(e.target.value)}`
  • buildFor callbacks that must always see the latest state, use a ref to store the latest value and read from the ref
  • buildUse the React DevTools profiler to see which renders produce which state values
  • buildConsider using useReducer for complex form state where multiple values depend on each other
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedType in a form field. Submit. The submitted value should match what is in the input.
  • verifiedTest rapid interactions: type quickly, submit, type again. No stale values should be submitted.
  • verifiedAdd a useEffect that logs the state value. It should always log the current value.
  • verifiedTest the form with React Strict Mode enabled — it surfaces stale closure bugs by double-invoking effects.
  • verifiedWrite a test that fills the form and submits, asserting the submitted payload matches the input.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningUsing an empty dependency array on useCallback as a 'performance optimisation' without understanding the trade-off
  • warningUsing defaultValue instead of controlled value + onChange for form inputs that need to reflect state changes
  • warningStoring derived state instead of computing it from props or other state
  • warningNot testing forms with React Strict Mode enabled
  • warningUsing setTimeout with a callback that closes over state without checking if the component is still mounted