Implement a polyfill for useRef
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.
useRef(initial) returns a mutable { current } object that has two defining properties:
- Stable identity — it's the same object across every render.
- Mutating
.currentdoes 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:
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.currentnever triggers a re-render. That's exactlyuseRefsemantics. - React preserves that state value across renders → stable identity.
Why the naive versions are wrong
function bad() {
return { current: initialValue }; // ❌ new object EVERY render — no stability
}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.