Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should component itself prevent unwanted useEffect() calls?

Using useEffect() properly is sometimes not that easy. Imagine we have the following simple app using the Counter component:

import { useState, useEffect } from 'react';

const Counter = ({ onOdd, onEven }) => {
    const [count, setCount] = useState(0);

    useEffect(
        () => {
            console.log('Inside useEffect()');

            if (count % 2 === 0) {
                onEven(count);
            } else {
                onOdd(count);
            }
        },
        [count, onOdd, onEven]
    );

    return (
        <button
            type="button"
            onClick={() => setCount(count => count + 1)}
        >
            {count}
        </button>
    );
}

const App = () => {
    const [isDarkMode, setIsDarkMode] = useState(false);

    return (
        <div style={{
            backgroundColor: isDarkMode ? 'black' : 'white',
        }}>
            <Counter
                onOdd={count => console.log(`Odd count: ${count}`)}
                onEven={count => console.log(`Even count: ${count}`)}
            />
            <button
                type="button"
                onClick={() => setIsDarkMode(isDarkMode => !isDarkMode)}
            >
                Toggle dark mode
            </button>
        </div>
    );
}

export default App;

The app does two things:

  1. It includes a Count button that increments its counter by 1. This components allows to inject two functions: onOdd and onEven. Whenever the counter changes, either onOdd or onEven is called, depending on the counter... being odd or even.
  2. There is also a dark mode toggle. The only purpose I added it is to have something that causes the Counter to re-render for other reason than changing the count.

Now, the app works with one quirk - whenever we toggle the dark/light mode, the onOdd or onEven is being called. That's wrong, but understandable - we're creating new functions on each render, so useEffect() is being called.

I can think of 4 ways to fix this behavior:

  1. Remove onOdd and onEven from useEffect() dependencies. It will fix the behavior, but it's considered a problem. The linter would complain about it, as we're losing data integrity. In theory, if we really change these callbacks, they should be re-run, right? That would be "the React way".
  2. Move the callback functions outside of the App component:
const onOdd = count => console.log(`Odd count: ${count}`);
const onEven = count => console.log(`Even count: ${count}`);

const App = () => {
    // ...
    return (
        // ...
            <Counter
                onOdd={onOdd}
                onEven={onEven}
            />
        // ...
    );
}

This is a good and fast solution, but it's only possible because we don't use hooks or state inside these callbacks. What if we did?

  1. Using useCallback() in App component:
const App = () => {
    // ...

    const onOdd = useCallback(
        count => console.log(`Odd count: ${count}`),
        []
    );

    const onEven = useCallback(
        count => console.log(`Even count: ${count}`),
        []
    );

    return (
        // ...
            <Counter
                onOdd={onOdd}
                onEven={onEven}
            />
        // ...
    );
}
  1. Memoizing the callback functions in Counter component. If we had thousands of components using Counter component, it would still mean only one place to memoize these functions. I'm not sure if that makes sense though.

How do React gurus approach this problem? I wanted to keep the example as simple as possible, so option #2 will work perfectly and would probably be preferable. But what if we needed to keep these callbacks inside the App component?

Is it always the parent component responsible to memoize all callbacks it passes to the child? If so, is it a recognized pattern to always memoize all functions passed as props (and perhaps any other objects) with useCallback() or useMemo()?

like image 394
Robo Robok Avatar asked Oct 25 '25 04:10

Robo Robok


1 Answers

I'm not properly a React Guru, but I consider all first three approaches to have their sweet spot, the 4th does not make sense. The only one to be careful with is the first one, since removing functions from deps, might lead to stale state issues, so if you know what you are doing, you may suppress lint warn ( I do that sometimes and know many others do that as it has been discussed extensiveley here https://github.com/facebook/react/issues/14920 ), otherwise it's better you avoid this approach.
The point number 2 is preferred everytime you have pure functions, always try to place your pure functions out of React components, inside some other folder like utils, misc, etc...
As per point number 3 that's the preferred way to handle functions declared inside React components, always memoize them with *useCallback* ( or useMemo if you need to perform calculations before to return a function ) , and there's nothing bad with doing that in the parent component. If you find yourself having dozens or hundreds of them and fear code pollution, consider that custom hooks let you to organize your code smartly, you could make a custom hook like useMemoizedHandlers inside your App component, where you create and memoize all your handlers and use it like:

const { 
        handler1,
        handler2,
        handler3
    } = useMemoizedHandlers()
like image 133
Cesare Polonara Avatar answered Oct 26 '25 19:10

Cesare Polonara