reactjsref

Difference Between useMemo and useEffect With Ref Dependencies


In React, both useEffect and useMemo have a dependencies argument.

I naively thought they worked identically: whenever the values in that dependencies argument changed, I thought the useEffect or useMemo callback would be run, and that the only difference was just timing (useMemo runs before the render, while useEffect runs after).

In general, they are identical. But when I have a ref as a dependency, they aren't:

const Foo = ()=> {
  const ref = useRef();
  useEffect(() => console.log('effect ref', ref), [ref]);
  useMemo(() => console.log('memo ref', ref), [ref]);
  return <div ref={ref}>Foo</div>
}

Sandbox Link (Click the console icon in the upper-right to see the logs)

As I understand it Foo should render twice. The first time through, ref.current will be undefined, and the second time it will be set to the <div>. Thus, I'd expect that to see two useEffect logs of effect ref div, because the ref will be set both after render #1 and render #2. And I do ...

effect ref {current: div}

effect ref {current: div}

However, with the memo logs I never see the div logged. Instead, I just see the same thing repeated:

memo ref undefined

memo ref undefined

That's expected for the first call, since the component hasn't rendered yet ... but before the second render, after the first, shouldn't the ref be set to the <div>, and so shouldn't useMemo log it? Instead, it seems useMemo is never run.

In short, while I mostly understand how useMemo works, it seems I'm missing some key detail that explains why it will never log a ref's value, and I'd appreciate any help explaining why.

P.S. I realize I could probably solve all this by making useRef depend on a state variable, and then making a useEffect which updates that state variable when the ref changes ... but I'm more focused on trying to understand the problem first before I solve it.


Solution

  • This behavior is due to React.StrictMode, not from using a ref as a dependency.

    A ref is a stable object reference across renders, and useEffect and useMemo use a referential equality check to determine if their dependencies have changed. Therefore, under normal circumstances (a prod build for example) these hooks would only run one time.

    Since you are seeing evidence that they are running more than once, the answer is Strict Mode.

    The explanation for this comes from a few different sections of the documentation.

    https://react.dev/reference/react/StrictMode

    Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup

    https://react.dev/reference/react/useMemo#caveats

    In Strict Mode, React will call your calculation function twice in order to help you find accidental impurities. This is development-only behavior and does not affect production. If your calculation function is pure (as it should be), this should not affect your logic. The result from one of the calls will be ignored.

    https://react.dev/blog/2022/03/29/react-v18#new-strict-mode-behaviors

    It's also possible (but impossible to tell) that it could be caused by React 18 StrictMode re-mounting every component.