How does useImperativeHandle() work? — React source code walkthrough

Usage

Here is the official example usage.

function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
function App() {
const ref = useRef();
const focus = useCallback(() => {
ref.current?.focus();
}, []);
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={focus} />
</div>
);
}

what if we just update ref.current?

Rather than useImperativeHandle(), what if we just update ref.current? like below.

function FancyInput(props, ref) {
const inputRef = useRef();
ref.current = () => ({
focus: () => {
inputRef.current.focus();
},
});
return <input ref={inputRef} />;
}
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
ref.current = () => ({
focus: () => {
inputRef.current.focus();
},
});
return () => {
ref.current = null;
};
}, [ref]);
return <input ref={inputRef} />;
}
function FancyInput(props, ref) {
const inputRef = useRef();
useEffect(() => {
if (typeof ref === "function") {
ref({
focus: () => {
inputRef.current.focus();
},
});
} else {
ref.current = () => ({
focus: () => {
inputRef.current.focus();
},
});
}
return () => {
if (typeof ref === "function") {
ref(null);
} else {
ref.current = null;
}
};
}, [ref]);
return <input ref={inputRef} />;
}

Ok let’s jump into the source code.

For effects, there is mount and update, which are different based on when useImperativeHandle() is called.

function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
function updateImperativeHandle<T>(
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null
): void {
// TODO: If deps are provided, should we skip comparing the ref itself?
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps
);
}
  1. under the hood mountEffectImpl and updateEffectImpl are used. useEffect() and useLayoutEffect() does the same, here and here
  2. the 2nd argument is HookLayout, which means it is layout effect.
function imperativeHandleEffect<T>(
create: () => T,
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void
) {
if (typeof ref === "function") {
const refCallback = ref;
const inst = create();
refCallback(inst);
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}

Wrap-up

useImperativeHandle() is no magic, it just wraps the ref setting and cleaning for us, internally it is in the same stage as useLayoutEffect() so a bit sooner than useEffect().

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store