Passing and Using Refs in React
Friday, May 7, 2021In our code base, we had a TextInput
component. This component helps with some focus managing, so it had a focusOnRender
prop that, when set, would focus this input when rendered. This is a simplication:
function TextInput({ focusOnRender, ...props }) {
const inputRef = useRef();
useEffect(() => {
if (focusOnRender) inputRef.current?.focus();
}, []);
return <input ref={inputRef} {...props} />;
}
When the input is rendered, it will be focused via the focusOnRender
prop.
As usage of this really basic TextInput evolved, we ended up wanting to expose a ref to other components. Normally, we would create the component like this:
const TextInput = forwardRef(
({ focusOnRender, ...props }, ref) => {
return <input ref={ref} {...props} />;
}
);
When using forwardRef, you cannot use the ref inside of the component.
For a while, we were able to use the use-forwarded-ref hook like this:
const TextInput = forwardRef(
({ focusOnRender, ...props }, ref) => {
const inputRef = useForwardedRef(ref);
useEffect(() => {
if (focusOnRender) inputRef.current?.focus();
}, []);
return <input ref={inputRef} {...props} />;
}
);
But for reasons I can't quite explain, this did not work. The forwarded ref would get discarded and be unusable by the higher components.
I found a better way in the React docs, using useImperativeHandle
. This hook customizes the value that is exposed in a ref. In a way, this is like exposing a ref API. You could use this handle to expose a specific API as a ref. That API would, of course, be used for imperative function calls.
const TextInput = forwardRef(
({ focusOnRender, ...props }, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
useEffect(() => {
if (focusOnRender) inputRef.current?.focus();
}, []);
return <input ref={inputRef} {...props} />;
}
);
This doesn't expose the same ref, per se, but allows you to selectively allow the functionality you want higher components to use.