Search code examples
reactjsreact-hooksuse-effect

How to get `useEffect` code reuse with custom hook that uses refs?


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

Solution

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