0

I need to provide some custom functionality after render that uses refs.

It seems to be a perfect use case for useEffect, but it needs to be in a custom hook so that it can be reused.

Unfortunately, passing either a ref or its current to a custom hook that does useEffect appears to result in different behaviour than calling useEffect directly.

A working example looks like this:

 const useCustomHookWithUseEffect = (el1, el2) => {
      React.useEffect(() => {
    console.log("CUSTOM Use effect...");
    console.log("firstRef element defined", !!el1);
    console.log("secondRef element", !!el2);
  }, [el1, el2]);
}

const RefDemo = () => {
    const [vis, setVis] = React.useState(false);
  
  const firstRef = React.useRef(null);
  const secondRef = React.useRef(null);
  
  useCustomHookWithUseEffect(firstRef.current, secondRef.current);
  
  React.useEffect(() => {
    console.log("Standard Use effect...");
    console.log("firstRef element defined", !!firstRef.current);
    console.log("secondRef element ", !!secondRef.current);
    }, [firstRef.current, secondRef.current]);
    
  console.log("At RefDemo render", !!firstRef.current , !!secondRef.current);
    
    return (
    <div>
      <div ref={firstRef}>
        My ref is created in the initial render
      </div>
      <div className="clickme" onClick={() => setVis(true)}>
        click me
      </div>
      {vis && 
        <div ref={secondRef}>boo (second ref must surely be valid now?)</div>
      }
    </div>
    )
}

After the first render, the custom hook does not have the defined value of firstRef, but the in-line useEffect does.

After clicking the click-me, once again the custom hook does not get the most-recent update (though now it has the firstRef value).

Is this expected?

How could I achieve the goal: be able to re-usably supply useEffect-based code that uses refs?

https://jsfiddle.net/GreenAsJade/na1Lstwu/34/


Here's the console log:

"At RefDemo render", false, false
"CUSTOM Use effect..."
"firstRef element defined", false
"secondRef element", false
"Standard Use effect..."
"firstRef element defined", true
"secondRef element ", false

Now I click the clickme

"At RefDemo render", true, false
"CUSTOM Use effect..."
"firstRef element defined", true
"secondRef element", false
"Standard Use effect..."
"firstRef element defined", true
"secondRef element ", true
GreenAsJade
  • 14,459
  • 11
  • 63
  • 98
  • https://stackoverflow.com/a/55370582/554807 looks similar, but the answer provided there is worse: if you provide the `ref` itself to the custom hook, it doesn't fire at all after click-me: presumably because the `ref` as a dependency to useEffect doesn't change. This can be seen here: https://jsfiddle.net/GreenAsJade/ehLsd7pf/1/ – GreenAsJade Jul 16 '22 at 08:40
  • Refs changing doesn't cause an update of the component. With that in mind, remember a custom hook is _just a function call_ with no magic whatsoever. – AKX Jul 16 '22 at 08:42
  • Given this, why does the behaviour of the 'custom hook' in this example differ from calling `useEffect` directly? And perhaps more importantly, how can I achieve the outcome I need? – GreenAsJade Jul 16 '22 at 08:43
  • @AKX Also, the ref changing _does_ trigger `useEffect`, when it is a dependency... which seemed like it would deliver what I needed – GreenAsJade Jul 16 '22 at 08:53
  • 1
    I'm pretty sure refs changing does not trigger an update by itself. Sure, if the reffed value is a dependency of an effect, the effect will be triggered if the value changes. Also remember React is at liberty of calling your component function whenever it likes, and how many times it likes (twice in StrictMode, generally)... – AKX Jul 16 '22 at 09:13
  • I'm definitely missing something in the point you're making. Your comment is confusing me because my question doesn't appear to have anything to do with when the component updates. It's about what happens when useEffect runs - and specifically when useEffect has a ref dependency.... – GreenAsJade Jul 16 '22 at 10:58
  • I see now: this answer lays it out https://stackoverflow.com/a/60476525/554807 – GreenAsJade Jul 16 '22 at 11:01

2 Answers2

1

The problem is that you pass the ref.current while rendering to the custom hook. Now when you change the state of vis, the component is executed from top to bottom again (effects are read, not yet executed). But in this rerender at the point you call the custom hook, your ref has not yet updated (since we have not yet actually rerendered and therefore assigned the ref to the second div). And since you specifically pass the value of the ref, it won`t show the actual updated ref value but the value you passed at the time of the function call (null). When the effect is then run later on, you only have access to the explicit value you passed, unlike if you would pass the ref itself, whose value will always be up to date. This code sandbox should illustrate the issue.

Dean
  • 521
  • 3
  • 14
  • Thanks - can you elaborate on how this causes a different result that the native useEffect, which has `current` as a dependency just like the one inside the hook? – GreenAsJade Jul 16 '22 at 22:08
  • putting the ref in the dependency wouldnt cause it to rerun the effect on any changes. but since you put the values as a dependency on both (your normal use effect and your custom hook both have the values as deps), they will run whenever the deps change. for the custom hook, the effect runs again because on the initial run, both values passed were null (as you can see in my code sandbox). on your rerender the values are now (first div, null) so it reruns. The different result relates back to my original answer: you pass the value decoupled of the ref into the hook.... – Dean Jul 16 '22 at 22:48
  • ...unlike your "normal" useEffect, that accesses the value over the ref object itself – Dean Jul 16 '22 at 22:49
  • Thanks for persisting, I got the lightglobe. It means that you can't make a custom hook with useEffect that behaves the same as a native useEffect, right? Because this naive example doesn't work: https://codesandbox.io/s/react-playground-forked-r874tb?file=/index.js (custom hook not called on ref update) – GreenAsJade Jul 17 '22 at 00:09
  • No, this is not what i'm saying. Your code sandbox works exactly like expected. both effects don't get called on the rerender, since you put only the ref objects itself in the dependencies (which does nothing). Both on the first render however, print the same results (which makes sense since they access both values over the refs). There is no difference between using the useEffect in a custom hook or directly in the function itself. it only depends on **how** you use it. – Dean Jul 17 '22 at 09:31
  • If this statement were true it would be wonderful: "There is no difference between using the useEffect in a custom hook or directly in the function itself. it only depends on how you use it". However, in the code in the question the direct call and the custom hook have the _same_ parameters passed to them, but they behave differently,. Furthermore, it does not seem possible to get the behaviour of the native call when using a custom hook. Could you demo a version that _does_ get the same _behaviour_ (even if it requires different calling)? – GreenAsJade Jul 17 '22 at 22:35
  • Your last sandbox (here in the comments) is an example of it being no different. They behave exactly the same. For the code sandbox in your original question, the difference is that they do NOT receive the same parameters. Your useEffect that's called in the render, accesses the values over the ref itself (refer to my original answer) while the custom hook receives the values at the time of rerendering (which are both null). In your CS in the comments you are passing the `ref` instead of `ref.current` into your custom hook, which is a big difference (which happens bcs of my original answer) – Dean Jul 18 '22 at 06:54
  • 1
    Thanks for persisting to make sure I got it right - that's gold. Here, finally, is how I learned from you to make the custom hook behaves the same as the native hook, which is what I was after ... https://jsfiddle.net/GreenAsJade/na1Lstwu/39/ Fingers crossed? – GreenAsJade Jul 19 '22 at 22:01
0

I realised that I asked two questions. This is the answer I found to "how should I achieve side effects from refs?"

The answer is "do not use refs in useEffect". Of course, there are situations where it might be fine, but it is certainly asking for trouble.

Instead, use a ref callback to achieve the side effect of the ref being created and destroyed.

The semantics of ref callback are much simpler to understand: you get a call when the node when it is created and another when it is destroyed.

GreenAsJade
  • 14,459
  • 11
  • 63
  • 98
  • ... but refs are still very painful: if your side-effect causes a re-render, then the ref goes away and comes back again ... causing your side-effect again... etc – GreenAsJade Jul 16 '22 at 23:48