DEV Community

loading...
Cover image for Merging refs

Merging refs

thekashey profile image Anton Korzunov ・6 min read

TL;DR - useRef and createRef are not the only API calls you might be interested in πŸ‘¨β€πŸ”¬

Let's imagine - you are opening a React Hooks API Reference, looking for the available hooks, and then... your eyes spot a strange one, you probably missed before - useImperativeHandle.
You never used it, and probably never needed it, and have no idea what it does. And the provided example is not very helpful to understand the use case.
So what is it?

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

Well, as I just said - that's not very helpful. I think nobody understands what's written here πŸ€”. Why it's written here πŸ€·β€β™‚οΈ. Let's fix this with a just one line.

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
+   // this is the PUBLIC API, we are exposing to a consumer via `ref`
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

In other words - once someone would <FancyInput ref={ref} /> that ref would contain only .focus.

Here is a codesanbox you can play with - https://codesandbox.io/s/react-hook-useimperativehandle-64e5j

The power of useImperativeHandle is the power of Bridge or, to be more concrete - a Facade pattern (you might know it as a wrapper).

However, useImperativeHandle is 100% useless, and powerless against the reality - I would prove it below.

Let's decompose everything to the atomic operations, to understand what do we actually need, and what's yet missing.

1. I want to do something when Ref changes

Let's imagine you want to expose different states depending on the ref value, because different refs might mean something different. However, the wrapper(and useImperativeHandle "exposes" a wrapper) you are exposing to the parent is always static. That's ok for some cases, but not ok for others. For example, you can't expose a real ref - only a wrapper around it, always a wrapper.

You need something like useImperativeHandle(ref, inputRef) to synchronize values between ref.current and inputRef.current, but it does not work that wat - only the useImperativeHandle(ref, () => inputRef.current) would work, but it will NOT update value when inputRef changes. There is no way you can react to inputRef.current changes

You might try to use dependency list for the useImperativeHandle hook to handle this situation. Something like:

useImperativeHandle( 
   ref, 
   () => inputRef.current, 
   [mmm, butWhen, itsGoing, toChange] // πŸ€·β€β™‚οΈ πŸ€”
);

Well, you do not depend on these dependencies(and eslint rules will not help you), but something you are attaching ref to - is. Even more - you might attach "your" ref to the same forwardProp component, which might do the same - you are not controlling what ref is. It's a black box, and there is no way you may safely predict how that blackbox works.

This pattern is very fragile and please don't ever use it. Keep in mind - the official documentation is asking not to use it as well.

The only way to make this pattern "stable" - is to know when ref is updated, and reactively update the parentRef.
How to do it? Use ref callback! And that is the official recommendation.

Hooks API Reference

And that's also not a great idea. Callbacks are not great, and probably that's why we have got RefObject.

How to use RefObject and be notified about the change? Easy, and es5 compatible.

function createCallbackRef(onChangeCallback) {
  let current = null;
  return {
    set current(newValue) {
      // new value set
      current=newValue;
      onChangeCallback(newValue);
    }
    get current() {
      // what shall I return?
      return current;
    }
  }
}

Object getters and setters are for the rescue. There is another article about this approach, explaining different use cases in depth:

2. I want to use Refs to DOM objects

useImperativeHandle is good, but it's not helpful when you need to work with real DOM elements.
The problem is still the same - to have a local ref, and synchronize it with the parent. You need to kill two refs with one rock. I mean - you need to update two like there is only one!

When developing low level UI components, it is common to have to use a local ref but also support an external one using React.forwardRef. Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility.

By fact - this is very needed and a very powerful feature, as useful as forwardRef itself - be able to maintain ref locally, still being transparent for the external consumer.

From some point of view, mergeRef IS a forwardRef, as long as it forwards a given ref, just to more than one location.

3. I want to update Refs

However, what is ref? It could be an object(ref object), it could be a function(callback ref). How to set the new value to the provided ref if you don't know what it is?

React and useImperativeHandle, as well as mergeRef, are hiding this logic inside, but you might need it in other cases. In useEffect for example.
So here you go:

function assignRef<T>(ref, value) {
  if (typeof ref === 'function') {
    ref(value);
  } else if (ref != null) {
    ref.current = value;
  }
  return ref;
}

If​ in the future ref's shape would change again, as it did with React 16 release, it would be just one function to update.

4. I want to transform my Refs

Which (surprise!) is what useImperativeHandle does - it gives you a programmatic way to return something else, derived from the original ref.

So, if you want to:

  • have a local ref, referring to the real DOM node
  • expose ref with .focus method to a parent

Then you have to merge and transform

-useImperativeHandle(ref, () => ({
-  focus: () => {
-    inputRef.current.focus();
-  }
-}));
mergeRefs([innerRef, tranformRef(ref, (current) => ({focus:() => current.focus()})

How to create transformRef? Well, we need some pieces we already had - callbackRef and assignRef

function transformRef(ref, transformer) {
  return createCallbackRef(
    value => assignRef(ref, transformer(value))
  )
}

transformRef is a real "bridge" between two worlds, for example in this issue someone needed to pass a "ref, stored in a ClassComponent instance, to the parent".

const FocusLock = ({As}) => <As ref={ref} />

const ResizableWithRef = forwardRef((props, ref) =>
  <Resizable {...props} ref={i => i && ref(i.resizable)}/>
  // i is a ref to a Class Component with .resizable property
);
// ...
<FocusLock as={ResizableWithRef}>
  • FocusLock expects ref to be a DOMNode
  • Resizable is a Class Component, so we are getting ref to a class instance
  • i => i && ref(i.resizable) - a callback ref - transfers i.resizable to a focus-lock ref, adaption to the API.

With transformRef it would look like

<Resizable {...props} ref={transformRef(ref, i => i.resizable)}/>

Looking the same, but does not require ref to be a callback ref - you don't need to worry about how you are going to do something, only about what you need to do.

5. I want my refs not to remount every time

Not sure you have noticed, but the code above (yes - all code above) would work, but would not make you (and React) happy.

The problem is: when ref is changing, React would set null to the old one, and then set the right value to the new one. And, as long as all functions above were returning a new function, or a new object every time - every time they would cause an update to the local, and parent refs.

And if you are using callback refs, and running some effects basing on their values - then it would cause even bigger updates.

Let's don't do that. Let's use hooks to memoize ref

function useTransformRef(ref, transformer) {
  return useState(() => createCallbackRef(
    value => assignRef(ref, transformer(value))
  ))[0];
}

Easy and sound - we are using useState with state fabric as an argument to create the ref only once, let our code execute more straightforward.

Conclusion

And there is a library, which already implemented all atoms I've provided above, tested and typed:

GitHub logo theKashey / use-callback-ref

πŸ€™The same useRef, but it will callback

πŸ€™ use-callback-ref πŸ“ž


The same `useRef` but it will callback: πŸ“ž Hello! Your ref was changed
Travis bundle size

Keep in mind that useRef doesn't notify you when its content changes Mutating the .current property doesn't cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead .... useCallbackRef instead.

– Hooks API Reference

Read more about use-callback pattern and use cases:

This library exposes helpers to handle any case related to ref lifecycle

  • useCallbackRef - react on hook change
  • mergeRefs - merge multiple refs together. For, actually, fork
  • transformRef - transform one ref to anther
  • refToCallback - convert RefObject to an old callback-style ref
  • assignRef - assign value to the ref, regardless of it's form

All functions are tree shakable, but even together it's less then 300b.

API

πŸ’‘ Some commands…




  • assignRef
  • useCallbackRef/createCallbackRef
  • useMergeRefs/mergeRefs
  • useTransformRef/transformRef
  • useRefToCallback/refToCallback

For the record: All kudos for mergeRefs goes to Greg Berge and react-merge-refs

Discussion (0)

pic
Editor guide