Implement a polyfill for useImperativeHandle
useImperativeHandle customizes the value a parent's ref sees when used with forwardRef. A polyfill: a hook that, given a ref and a factory, assigns the factory's result to ref.current in a layout effect and cleans up on unmount — keyed on deps.
useImperativeHandle(ref, createHandle, deps) lets a child customize what a parent gets through a forwarded ref — instead of the raw DOM node, the parent sees an object you define (e.g. { focus, scrollToTop }).
What it does
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ""; },
}), []);
return <input ref={inputRef} />;
});
// parent: fancyRef.current.focus() — not the DOM node, the custom handleThe polyfill
The behavior: take whatever ref the parent passed (object ref or callback ref), and assign createHandle()'s result into it — synchronously after render (a layout effect, so the handle exists before paint), re-running when deps change, and cleaning up on unmount.
function useImperativeHandlePolyfill(ref, createHandle, deps) {
useLayoutEffect(() => {
if (!ref) return;
const handle = createHandle();
// support both object refs and callback refs
if (typeof ref === "function") {
ref(handle);
} else {
ref.current = handle;
}
// cleanup: clear the ref on unmount / before re-running
return () => {
if (typeof ref === "function") ref(null);
else ref.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}The key design points
useLayoutEffect, notuseEffect— the imperative handle must be in place before the browser paints and before the parent's effects run, so the parent can call it immediately. The real hook attaches it during the commit phase.- Handle both ref forms —
refcan be an object ({ current }) or a callback function; the polyfill must support both, exactly likeforwardRefdoes. - Cleanup — null out
ref.current(or callref(null)) on unmount so the parent doesn't hold a stale handle. deps— recreate the handle only when dependencies change; the closure insidecreateHandleotherwise captures stale values.
Why interviewers ask
It tests whether you understand that useImperativeHandle is "just" a layout effect that writes to a ref, plus the subtlety of object-vs-callback refs and timing.
The framing
"useImperativeHandle customizes what a parent's forwarded ref sees. The polyfill is a hook that, in a layout effect — so the handle exists before paint — calls the factory and writes its result into the passed ref, supporting both object and callback refs, with cleanup that nulls it on unmount, all keyed on the deps array. The non-obvious parts are using useLayoutEffect for timing and handling both ref shapes."
Follow-up questions
- •Why must this use useLayoutEffect instead of useEffect?
- •How do you support both object refs and callback refs?
- •Why is useImperativeHandle considered an escape hatch?
- •What happens if you omit the deps array?
Common mistakes
- •Using useEffect — the handle wouldn't exist early enough for the parent.
- •Only handling object refs, not callback refs.
- •Forgetting cleanup, leaving a stale handle on the parent's ref.
- •Ignoring deps, so the handle closes over stale values.
Performance considerations
- •Negligible cost — one layout effect. The deps array matters: a missing/empty deps with closures over changing values causes stale-handle bugs, not perf issues.
Edge cases
- •ref is null (parent didn't pass one).
- •Parent swaps the ref between renders.
- •createHandle returning a new object every render with no deps.
Real-world examples
- •Exposing focus()/scrollIntoView() from a custom input or modal component.
- •Library components exposing imperative methods (a video player's play/pause).