Avoiding unexpected calls from useEffect on re-renders with useCallback and useRef approaches

Avoiding unexpected calls from useEffect on re-renders with useCallback and useRef approaches

passing callbacks from one component down to another on props is a cause of unexpected multiple calls on re-renders

The practice of put in place useCallback reactjs.org/docs/hooks-reference.html#useca.. is useful because guarantees that a function won't change unless its dependencies change, and making sure we can pass down that function to any other component as a prop and it won't end with an undesired behavior, like useEffect being executed multiple times.

Goal

The useEffect in the sample should be only executed once, when component is mounted, and not every time the button is clicked and changing unrelated state and parent being re-rendered

Case 1: Showing unexpected behavior

Every time you click on the button that sets the counter, the callback is triggered, because, on every render, the callback value changes, making the useEffect to call again the function.

This will end in multiple times logging:

console.log("callback value", val);

App.js

import { useState } from "react";
import { CallCallbackV1 } from "./CallCallbackV1";

export default function App() {
  const [count, setCount] = useState(0);
  const callback = (val) => {
    console.log("callback value", val);
  };
  return (
    <div className="App">
      <CallCallbackV1 callback={callback} />
      <div>
        {count}
        <button onClick={(_) => setCount((state) => state + 1)}>
          Count Button
        </button>
      </div>
    </div>
  );
}

CallCallbackV1.js

import { useEffect } from "react";

export const CallCallbackV1 = ({ callback }) => {
  const something = "Internal value";
  useEffect(() => {
    callback(something);
  }, [callback, something]);

  return <div>CallCallbackV1</div>;
};

Case 2: Fixing the issue with useCallback

Now we fix the unexpected calls by using useCallback, if you click multiple times the button, only 1 time will be triggered the callback from the component, because useCallback memoize the function so it persist the same value on every render and update only if dependencies array items are changed.

App.js

import { useCallback, useState } from "react";
import { CallCallbackV1 } from "./CallCallbackV1";

export default function App() {
  const [count, setCount] = useState(0);

  const callbackWithUseCallback = useCallback((val) => {
    console.log("callback with useCallback value", val);
  }, []);
  return (
    <div className="App">
      <CallCallbackV1 callback={callbackWithUseCallback} />
      <div>
        {count}
        <button onClick={(_) => setCount((state) => state + 1)}>
          Count Button
        </button>
      </div>
    </div>
  );
}

Case 3: Fixing the issue with useRef

useRef reactjs.org/docs/hooks-reference.html#useref main goal is to reference DOM elements, or if you want to initialize a third-party library that needs DOM access.

Tanner Linsley (creator of react query) on youtube.com/watch?v=J-g9ZJha8FE on his hooks presentation, describes another interesting way to avoid the verbosity of useCallback, and I think is more useful when we create a component reusable.

App.js

  const callback2 = (val) => {
    console.log("callback with useRef value", val);
  };


  return (
    <div className="App">
       ...
      <CallCallbackV2 callback={callback2} />

CallCallbackV2.js

import { useEffect, useRef } from "react";

export const CallCallbackV2 = ({ callback }) => {
  const something = "Internal value";
  const callBackRef = useRef();
  callBackRef.current = callback;
  useEffect(() => {
    callBackRef.current(something);
  }, [callBackRef, something]);

  return <div>CallCallbackV2</div>;
};

Source code

codesandbox.io/s/vibrant-sun-jfuql?file=/sr..

But why useRef makes code to work as expected ?

Would like to reference React's documentation :

Essentially, useRef is like a “box” that can hold a mutable value in its .current property.

Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render.

Even though callBackRef.current is being modified with callback function provided from the parent component on every re-render, for the useEffect, the callBackRef value has not changed, so that is why our code is fixed, and the callback is executed once as expected.