TL;DR -
useRef
andcreateRef
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 betweenref.current
andinputRef.current
, but it does not work that wat - only theuseImperativeHandle(ref, () => inputRef.current)
would work, but it will NOT update value wheninputRef
changes. There is no way you can react toinputRef.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.
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
expectsref
to be aDOMNode
-
Resizable
is a Class Component, so we are gettingref
to a class instance -
i => i && ref(i.resizable)
- a callback ref - transfersi.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:
theKashey / use-callback-ref
🤙The same useRef, but it will callback
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.
Read more about use-callback
pattern and use cases:
This library exposes helpers to handle any case related to ref
lifecycle
-
useCallbackRef
- react on a ref change (replacement foruseRef
)-
createCallbackRef
- - low level version ofuseCallbackRef
-
-
useMergeRefs
- merge multiple refs together creating a stable return ref-
mergeRefs
- low level version ofuseMergeRefs
-
-
useTransformRef
- transform one ref to another (replacement foruseImperativeHandle
)-
transformRef
- low level version ofuseTransformRef
-
-
useRefToCallback
- convert RefObject…
assignRef
-
useCallbackRef
/createCallbackRef
-
useMergeRefs
/mergeRefs
-
useTransformRef
/transformRef
-
useRefToCallback
/refToCallback
For the record: All kudos for
mergeRefs
goes to Greg Berge and react-merge-refs
Top comments (1)
Very helpful article. Thanks so much for taking the time to write it.