Skip to content

Chapter 3 of 7

Refs, Stale Closures, and Imperative Escapes

useRef returns a mutable container whose .current property persists across renders without triggering a re-render when mutated. That makes it the right tool for storing DOM nodes, timer ids, the latest value of some state, instance-like variables on a function component, and WebSocket or observer handles. Because writing to .current does not schedule a render, refs are also the standard escape hatch when you need to read or mutate something synchronously inside a callback. Two TypeScript flavors matter: RefObject<T> exposes { current: T \| null } and is set by React, while MutableRefObject<T> exposes { current: T } and is something you manage directly.

The canonical pattern for reading the latest state inside a long-lived callback is the ref-plus-effect combo. You create a ref, then in an effect you assign the current state into the ref on every render. Inside the callback — say, an interval or an event listener — you read ref.current instead of the closed-over state variable. This is how you fix the classic stale-closure trap where setInterval(() => console.log(count), 1000) logs the initial value forever because the callback captured the count from the first render. A usePrev hook (returning the value from the previous render) and a useIsMounted hook (returning whether the component is still mounted) follow the same pattern: assign in an effect, read in callbacks. Stable callbacks that always see the latest state have been an open problem; the community pattern combines useRef with useCallback to produce a function whose identity never changes but whose body always reads the freshest state, and the official useEvent / useEffectEvent RFC (available in React canary) formalizes this with a stable callback that closes over the latest values without forcing effect deps to change.

Refs also escape the declarative model when you genuinely need imperative handles. forwardRef passes a ref through a component to a child DOM node, and useImperativeHandle customizes what the parent sees — for example, exposing only { focus, clear } instead of the raw DOM element. React 19 simplifies this significantly: function components now accept ref as a regular prop, removing the need for forwardRef in most cases. Ref callbacks themselves gained a superpower: in React 19, a ref callback can return a cleanup function that runs when the ref is detached, which makes merge-refs utilities and one-off subscriptions cleaner. A simple merge helper combines multiple refs into one callback that fans out to each. Use these imperative tools sparingly — exposing internal state up the tree is usually a sign you should lift that state to a parent instead.

All chapters
  1. 1Foundations: Rules, StrictMode, and the Hook Mental Model
  2. 2useEffect and Side Effects: Patterns and Pitfalls
  3. 3Refs, Stale Closures, and Imperative Escapes
  4. 4Memoization and Render Performance
  5. 5State Management: useReducer, Context, and External Stores
  6. 6Custom Hooks: Reusable Patterns
  7. 7Concurrent Features, React 19 Hooks, and Server Architecture

Drill it

Reading is not remembering. These come from the React Hooks Advanced Patterns deck:

Q

Why does useEffect run twice in dev?

Strict Mode mounts → unmounts → remounts to catch bugs from missing cleanups. Production runs effects once per mount.

Q

Common cause of infinite useEffect loop?

Updating state inside the effect with a dep that changes when state updates. Memoize the value or remove from deps if it doesn't need to trigger.

Q

How does useCallback help?

Returns the same function reference unless deps change — avoids re-rendering children that take it as a prop and use memo().

Q

useMemo vs useCallback?

useMemo(f, d): memoizes a value (the result of f()).useCallback(f, d): memoizes the function itself. Equivalent to useMemo(() => f, d).