DEV Community

loading...
Cover image for Concrete example for React.useImperativeHandle

Concrete example for React.useImperativeHandle

poeticgeek profile image Yoav Niran ・3 min read

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Alt Text

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!

Alt Text

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} />
      );
    }
  );

Enter fullscreen mode Exit fullscreen mode

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;
  }
`;
Enter fullscreen mode Exit fullscreen mode

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

The important part in the code above, of course, is the call to useImperativeHandle:


  useImperativeHandle(ref, () => internalRef.current, []);
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (4)

pic
Editor guide
Collapse
deadcoder0904 profile image
Akshay Kadam (A2K)

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...

Collapse
kentcdodds profile image
Kent C. Dodds

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:


const NumberInput = forwardRef((props, ref) => {
  const onUp = useCallback(() => {
    ref.current.stepUp()
  }, [ref])

  const onDown = useCallback(() => {
    ref.current.stepDown()
  }, [ref])

  return (
    <NumberInputWrapper>
      <TextInput {...props} type="number" ref={ref} />
      <NumberButtonsContainer>
        <NumberButton onClick={onUp}>⬆️</NumberButton>
        <NumberButton onClick={onDown}>⬇️</NumberButton>
      </NumberButtonsContainer>
    </NumberInputWrapper>
  )
})
Enter fullscreen mode Exit fullscreen mode

This seems to work just the same for me. Am I missing something?

codesandbox

Collapse
poeticgeek profile image
Yoav Niran Author

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)

Collapse
donysukardi profile image
Dony Sukardi

Great find Yoav! This is a very nice and simple way to compose external and internal refs.