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.