A relatively obscure tool in the React hooks toolbox is useImperativeHandle. Despite being around for a quite a while.
Most of the time, it's not needed and even the docs are discouraging its use, opting for more declarative solutions.
At times, it can prove useful. In this post I'd like to show one use that we @Cloudinary recently found.
Deeper look
First, let's take a closer look at the hook's implementation.
As with other hooks, the actual implementation is published as part of the react-dom package and not in react.
function imperativeHandleEffect(create, ref) {
if (typeof ref === 'function') {
ref(create());
} else if (ref !== null && ref !== undefined) {
ref.current = create();
}
}
The code above is a great simplification. The actual code is here.
This function is wrapped by a mountEffect() which means it runs just like useEffect.
As we can see, useImperativeHandle will run our create
function and will assign it to the ref
parameter. If its a function it will be passed as input, otherwise, it will become the .current value.
useImperativeHandle accepts a dependency list, as with other hooks. However, React will actually add the
ref
to the list of dependencies for you but eslint-plugin-react-hooks doesnt seem to know that.
Challenge
So, what can you do with it beyond the simple example ReactJS provides?
Well, in our case, we're building our own UI Components as part of a design system.
We have a TextField component that we wrapped with a new component: NumberField. For the most part, NumberField is very similar to its Text counterpart. However, we wanted a consistent behavior and look&feel for its up/down buttons.
These however, look different cross browser so we needed our own UI.
Then came the challenging part - how do you control the value of the input from React-land without forcing it into a controlled component? The use of the component should determine if its controlled or not. So the component itself, shouldn't.
A colleague of mine pointed me to the very useful HTMLInputElement.stepUp() and HTMLInputElement.stepDown() methods. This meant we can change the input's value without passing value
.
Great!
But NumberField just wraps TextField. So it needs to be able to use its own ref while passing an outside ref to the inner TextField.
Another constraint - ref might be a function or it may be an object (result of useRef). So we need to support both (sounds familiar?).
Here, useImperativeHandle comes to the rescue. It's not like we couldn't solve the issue without it. It just reduced the solution to a very concise, one liner. Whoo!
Code
First, we define our TextInput. Simplified of course for the purpose of this article.
const TextInput = forwardRef(
({ type = "text", defaultValue, value, onChange, className }, ref) => {
return (
<input className={className} type={type} ref={ref} value={value} defaultValue={defaultValue} onChange={onChange} />
);
}
);
Next, we define a container for our number input that will hide the native up/down buttons.
const NumberInputWrapper = styled.div`
display: flex;
input[type="number"] {
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
appearance: textfield;
}
`;
Finally, we define our NumberInput.
const NumberInput = forwardRef((props, ref) => {
const internalRef = useRef(null);
useImperativeHandle(ref, () => internalRef.current, []);
const onUp = useCallback(() => {
internalRef.current.stepUp();
}, [internalRef]);
const onDown = useCallback(() => {
internalRef.current.stepDown();
}, [internalRef]);
return (
<NumberInputWrapper>
<TextInput {...props} type="number" ref={internalRef} />
<NumberButtonsContainer>
<NumberButton onClick={onUp}>⬆️</NumberButton>
<NumberButton onClick={onDown}>⬇️</NumberButton>
</NumberButtonsContainer>
</NumberInputWrapper>
);
});
The important part in the code above, of course, is the call to useImperativeHandle:
useImperativeHandle(ref, () => internalRef.current, []);
the first argument is the ref we received from outside. Inside the create
function, we return the result of our internal ref. This will make it possible for the code outside to use the ref as before. Internally, we can use our internalRef instance to make changes to the input through the DOM.
Simple!
P.S. Full code example can be found in this codesandbox.
Top comments (5)
Hey Yoav, really nice example. Can we use
useImperativeHandle
to share refs between siblings?For example, I want to target a
ref
in one sibling component & access it from other sibling, here's a full description of what I mean → stackoverflow.com/questions/654763...The simple answer is yes. You can use it just like any other ref.
Architecturally, I'm not sure that's the best approach. As you (or someone else) may end up changing the "API" of the obj you assign the ref in one side without realizing its effect on its sibling.
In general, I don't think you want to build dependencies between sibling components this way.
Hey Yoav! I'm not sure you needed to use useImperativeHandle here. Unless I'm missing something, you could just as easily have referenced the given ref by itself:
This seems to work just the same for me. Am I missing something?
codesandbox
Hi Kent. Sure, in this simplified and specific example. :)
However, what if for example, ref was a function? Then you'd have to check it and deal with both scenarios.
What if ref wasn't passed at all? It could be that NumberInput is needed as a controlled component. You'd still need the internal ref for the stepUp and stepDown methods.
This way, you separate the usage of external and internal ref, and you make the connection if needed very easily (taking care of both function and object flavors)
Great find Yoav! This is a very nice and simple way to compose external and internal refs.