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?
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.
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 differentrefs 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:
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.
functioncreateCallbackRef(onChangeCallback){letcurrent=null;return{setcurrent(newValue){// new value setcurrent=newValue;onChangeCallback(newValue);}getcurrent(){// what shall I return?returncurrent;}}}
Object getters and setters are for the rescue. There is another article about this approach, explaining different use cases in depth:
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!
Greg Bergé
@neoziro
Just published react-merge-refs, a React utility to use a local and an external ref!
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:
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".
constFocusLock=({As})=><Asref={ref}/>
constResizableWithRef=forwardRef((props,ref)=><Resizable{...props}ref={i=>i&&ref(i.resizable)}/>
// i is a ref to a Class Component with .resizable property);// ...<FocusLockas={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 - transfersi.resizable to a focus-lock ref, adaption to the API.
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
The same `useRef` but it will callback: 📞 Hello! Your ref was changed
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.