Back to React
React
easy
mid

How would you implement a polyfill for the useRef hook?

useRef returns a stable mutable object { current } that persists across renders and does NOT trigger a re-render when mutated. The polyfill: use useState's lazy initializer once to create and hold a single { current } object — the trick is reusing the SAME object every render.

4 min read·~10 min to think through

useRef(initial) returns a mutable { current } object that has two defining properties:

  1. Stable identity — it's the same object across every render.
  2. Mutating .current does NOT trigger a re-render.

The polyfill

The trick: you need a value that's created once and reused. useState's lazy initializer runs only on the first render, and state values are preserved across renders — so:

js
function useRefPolyfill(initialValue) {
  // lazy initializer runs ONCE; the same object is returned every render
  const [ref] = useState(() => ({ current: initialValue }));
  return ref;
}
  • The factory () => ({ current: initialValue }) runs only on mount → one object, created once.
  • We never call the setter → mutating ref.current never triggers a re-render. That's exactly useRef semantics.
  • React preserves that state value across renders → stable identity.

Why the naive versions are wrong

js
function bad() {
  return { current: initialValue }; // ❌ new object EVERY render — no stability
}
js
function alsoBad() {
  const [ref] = useState({ current: initialValue }); // ⚠️ eager — recreates the object arg each render
}                                                    //    (React discards it, but it's wasteful)

The lazy initializer (() => ...) is the key — it's what guarantees the object is built exactly once.

Building it on useMemo instead

useMemo(() => ({ current: initialValue }), []) usually works too — but React explicitly reserves the right to discard memoized values, so it's not a guaranteed-stable container. useState storage is guaranteed to persist. That's why the real useRef is its own primitive and not just useMemo.

Why interviewers ask

It tests whether you understand the two things useRef guarantees (stability + no re-render) and that the mechanism is "persistent storage created once" — the lazy initializer is the insight.

The framing

"useRef is a stable { current } object that survives renders and whose mutation never re-renders. I'd polyfill it with useState's lazy initializer: const [ref] = useState(() => ({ current: initial })) — the factory runs once so there's a single object, and since I never call the setter, mutating .current can't trigger a render. The naive bug is returning a fresh object each render and losing stability; useMemo is close but React may discard memoized values, so useState storage is the correct backing."

Follow-up questions

  • Why does the lazy initializer matter here?
  • Why not just use useMemo for this?
  • Why doesn't mutating ref.current cause a re-render?
  • What's the difference between useRef for DOM nodes vs instance variables?

Common mistakes

  • Returning a new { current } object every render — loses stability.
  • Using useState eagerly instead of with a lazy initializer.
  • Assuming useMemo is a guaranteed-stable replacement.
  • Thinking useRef should trigger re-renders on change.

Performance considerations

  • useRef is essentially free — one object stored once. Its value is that it lets you hold mutable data across renders without the re-render cost of state.

Edge cases

  • Initial value that's expensive to compute — lazy init avoids recomputing it.
  • Mutating .current and expecting the UI to update (it won't).
  • Using the ref before it's attached to a DOM node (current is null).

Real-world examples

  • Holding a DOM node reference, a timer id, or a 'previous value' across renders.
  • Storing the latest value of a prop/state for use in a stable callback without re-subscribing.

Senior engineer discussion

Seniors name both guarantees (stable identity, no re-render), build it on useState's lazy initializer, and can explain why useMemo isn't a sound substitute — React may evict memoized values, while state storage is preserved.

Related questions