-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React 18 let's make ref.currant to be reactive value #21903
Comments
Making refs "reactive" as you say would essentially make them the same as the Edit To be clear, the issue is not that a change in ref value won't re-run an effect. It's that a change in ref value won't re-render a component. (If something else happens to re-render the component, and a ref value is passed in as a dependency– the effect would re-run. The reason we advise against using refs in the dependencies array is that changes to refs don't cause a component to re-render in the first place, as dependencies coming from state do.) |
useCallback is sugar on top of useMemo, but why did we add useCallback when there is already useMemo? Because useCallback is a common use case. useState is sugar on top of useReducer, but we anyway added useState. People often mistakenly try to specify ref.current as dependencies of other hooks. And they often make a mistake, because ref.current is not a reactive value. To make ref.current reactive, people would have to write the same hook many times, which facebook could implement and insert into the core. The implementation I wrote is only approximate. If facebook can make a better implementation that somehow won't cause the component to re-render, then great. refs serve for imperative interaction with elements. useEffect also serves for imperative interaction. Obviously, useEffect will want to use |
Can you share some examples of how you'd use it? Meaning concrete examples where you want |
@gaearon Effects have a handy "effect undo" ability const ref = useRef(null)
useEffect(()=> {
const observer = new ResizeObserver()
observer.observe(ref.current)
return () => {
observer.disconnect()
}
}, [ref.current])
return <div ref={ref}></div> If we just used a functional ref, then we would have to store the previous ResizeObserver somewhere in order to destroy it later const callbackRef = useCallback((element) => {
//////////////////////////////////////////
// where to store previoutObserver?
if(previoutObserver) {
previoutObserver.disconnect()
}
const observer = new ResizeObserver()
observer.observe(element)
}, [])
return <div ref={callbackRef}></div> Maybe it is worth adding functionality in which functional refs would also return a cancellation function that will be executed when an element is changed or unmounted? this would achieve the same behavior as useEffect, but without re-rendering the component |
The example above already works without anything in the dependencies array: const ref = useRef(null);
useEffect(() => {
const observer = new ResizeObserver();
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, []);
return <div ref={ref}></div>; Since function Example({ showDiv }) {
const ref = useRef(null);
useEffect(() => {
if (showDiv) {
const observer = new ResizeObserver();
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}
}, [showDiv]);
return showDiv ? <div ref={ref}></div> : null;
} Could you maybe provide another example that doesn't work with the current |
const ref = useRef(null);
useEffect(() => {
const observer = new ResizeObserver();
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref.current]);
// we don't know when child will change ref.current and how it will use it, but we want
return <Child prop={ref}/>; My new idea is to give functional refs the ability to return a function that will be executed when ref changes, Similar to how useEffect has it. I understand that there is no need to overload the core api with optional hooks, but this functionality does not add new hooks like const funcRef= useCallback((element) => {
const observer = new ResizeObserver();
observer.observe(element);
// this function will called on ref change,
// to undo actions made during the previous call,
// and potentially necessary variables will be in the closure
return () => {
observer.disconnect();
};
}, []);
return <div>
{ state && <div ref={funcRef}></div> }
</div> |
Thanks for explaining the use case. This is a known inconvenience. Internally we’ve seen people make a custom The current built-in canonical solution to this is callback refs, but it’s a bit awkward that callback refs have a different API from effects. |
Just to spell it out a little more explicitly, one way to approach this using a callback ref would be: const cleanupRef = useRef(null);
const refSetterFunction = useCallback((element) => {
if (cleanupRef.current !== null) {
// Either the <div> has been hidden, or a value in the dependencies array has changed.
// Either way, this is the time to cleanup.
cleanupRef.current();
cleanupRef.current = null;
}
if (element !== null) {
// Either the <div> has been shown, or a value in the dependencies array has changed.
// Either way, this is the time to recreate our effect.
const observer = new ResizeObserver();
observer.observe(element);
// Store for later cleanup (when <div> is hidden or dependencies change)
cleanupRef.current = () => {
observer.disconnect();
};
}
}, []); Dependencies aren't used in the example callback, but they could be added if needed. |
@bvaughn Why don't we increase the level of abstraction and convenience, and not just use a closure to store the variables needed for cleaning, This is my suggestion =) Compare this: const cleanupRef = useRef(null);
const refSetterFunction = useCallback((element) => {
if (cleanupRef.current !== null) {
// Either the <div> has been hidden, or a value in the dependencies array has changed.
// Either way, this is the time to cleanup.
cleanupRef.current();
cleanupRef.current = null;
}
if (element !== null) {
// Either the <div> has been shown, or a value in the dependencies array has changed.
// Either way, this is the time to recreate our effect.
const observer = new ResizeObserver();
observer.observe(element);
// Store for later cleanup (when <div> is hidden or dependencies change)
cleanupRef.current = () => {
observer.disconnect();
};
}
}, []); vs this: const refSetterFunction = useCallback((element) => {
if(!element) return
const observer = new ResizeObserver()
observer.observe(element)
return () => {
observer.disconnect()
};
}, []); Is there a person on this earth who will say that the first variant is better? =) @gaearon Yes, I mean the same. If people create something often, and do it in different ways, and perhaps not correctly and not optimally, this is a sign that this functionality needs to be added to the core, not to the user space. do you agree? I believe that version 18 of react is just right for such an update, let functional refs have the ability to return a "cancellation function", this will cause a minimal loss of backward compatibility, since few people "accidentally" returned a function before, but it will also benefit the general consistency of the api useEffect and callback refs, and give the React api a consistent style, here's my opinion I cannot influence react directly, but I would like to provide at least some help, feedback, from the side of the react user community. that's why I'm writing all this, especially since the 18th version has not yet been released and there is time to have time to implement this trifle hope the React team agrees it =) |
Related to #15176 and reactjs/rfcs#205 |
想要ref.current能触发render?这和react设计的理念背道而驰啊。 |
I've wondering about this for a very long time. It'd be super helpful if callbacks refs could return a cleanup function. |
Let's add this hook as part of the core. Since this is a common need, many people often ask the question "Why does useEffect not sense ref.current changes?"
Usage example:
The text was updated successfully, but these errors were encountered: