Skip to content
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

Open
MaxmaxmaximusAWS opened this issue Jul 17, 2021 · 12 comments
Open

React 18 let's make ref.currant to be reactive value #21903

MaxmaxmaximusAWS opened this issue Jul 17, 2021 · 12 comments
Labels
React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion

Comments

@MaxmaxmaximusAWS
Copy link

MaxmaxmaximusAWS commented Jul 17, 2021

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?"

// approximate implementation
const useReactiveRef = (defaultValue) => {
  const [current, ref] = useState(defaultValue)
  ref.current = current
  return ref
}

Usage example:

const Component = () => {
  const ref = React.useReactiveRef()

  useEffect(() => {
    // ref.current now is reactive
    console.log(ref.current)
  }, [ref.current])

  return <div ref={ref}></div>
}
@MaxmaxmaximusAWS MaxmaxmaximusAWS added React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion labels Jul 17, 2021
@bvaughn
Copy link
Contributor

bvaughn commented Jul 18, 2021

Making refs "reactive" as you say would essentially make them the same as the useState hook. If you want a value that ensures a re-render on change, use the state hook. Can you explain why that would not be sufficient?


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

@MaxmaxmaximusAWS
Copy link
Author

MaxmaxmaximusAWS commented Jul 20, 2021

Can you explain why that would not be sufficient?

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 ref.current as a dependency, this is a common use case

@gaearon
Copy link
Collaborator

gaearon commented Jul 20, 2021

Can you share some examples of how you'd use it? Meaning concrete examples where you want .current to be a dependency. I'd like to better understand the scenarios you're describing.

@MaxmaxmaximusAWS
Copy link
Author

MaxmaxmaximusAWS commented Jul 20, 2021

@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

@bvaughn
Copy link
Contributor

bvaughn commented Jul 20, 2021

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 <div> isn't conditionally rendered, and the effect runs whenever the component is mounted/shown, then the two already align. If <div> was conditionally rendered, then the prop or state that determined the render could be used instead. For example:

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 useRef API?

@MaxmaxmaximusAWS
Copy link
Author

MaxmaxmaximusAWS commented Jul 21, 2021

@bvaughn

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 useReactiveRef, and in fact is backward compatible. If someone accidentally returned a function before, and does not expect it to be called, we honestly up a major version React to 18, to point out possible compatibility issues. We need to be able to store intermediate data somewhere between calls functional ref, and this is a closure.

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>

@gaearon
Copy link
Collaborator

gaearon commented Jul 21, 2021

Thanks for explaining the use case. This is a known inconvenience. Internally we’ve seen people make a custom useEffectRef Hook that does what you suggest.

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.

@bvaughn
Copy link
Contributor

bvaughn commented Jul 21, 2021

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.

@MaxmaxmaximusAWS
Copy link
Author

MaxmaxmaximusAWS commented Jul 21, 2021

@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 make a custom useEffectRef Hook

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 =)

@KurtGokhan
Copy link

Related to #15176 and reactjs/rfcs#205

@heyuuuu
Copy link

heyuuuu commented Jul 26, 2022

想要ref.current能触发render?这和react设计的理念背道而驰啊。

@lgenzelis
Copy link

I've wondering about this for a very long time. It'd be super helpful if callbacks refs could return a cleanup function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion
Projects
None yet
Development

No branches or pull requests

6 participants