Home > Software engineering >  React.useMemo is re-rendering all items in array
React.useMemo is re-rendering all items in array

Time:12-10

I have a react component that stores a set of fruits in useState. I have a memoized function (visibleFruits) that filters fruits. I map visibleFruits to the dom.

The problem is, when i check a fruit, all visible fruits re-render.

I am expecting that only the selected one re-renders since it is the only one that is changing.

Is there a way for me to use this pattern but prevent all from re-rendering on check?

In real life, there is a complex function in visibleFruits useMemo. So I can't simply append the filter before the map.

Edit, here is updated edit:


const Example = () => {
    const [fruits, setFruits] = React.useState([
        { id: 1, name: 'apple', visible: true, selected: false },
        { id: 2, name: 'banana', visible: false, selected: false },
        { id: 3, name: 'orange', visible: true, selected: false }
    ])

    const visibleFruits = React.useMemo(() => {
        return fruits.filter((f) => f.visible)
    }, [fruits])

    const handleCheck = (bool, id) => {
        setFruits((prev) => {
            return prev.map((f) => {
                if (f.id === id) {
                    f.selected = bool
                }
                return f
            })
        })
    }

    return (
        <div>
            {visibleFruits.map((fruit) => {
                return <FruitOption fruit={fruit} handleCheck={handleCheck} />
            })}
        </div>
    )
}

const FruitOption = ({ fruit, handleCheck }) => {
    console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
    return (
        <div key={fruit.id}>
            <input
                checked={fruit.selected}
                onChange={(e) => handleCheck(e.target.checked, fruit.id)}
                type='checkbox'
            />
            <label>{fruit.name}</label>
        </div>
    )
}

export default Example

CodePudding user response:

First, there's a problem with the handleCheck function (but it's not related to what you're asking about). Your code is modifying a fruit object directly (f.selected = bool), but you're not allowed to do that with React state, objects in state must not be directly modified, and rendering may not be correct if you break that rule. Instead, you need to copy the object and modify the copy (like you are with the array):

const handleCheck = (bool, id) => {
    setFruits((prev) => {
        return prev.map((f) => {
            if (f.id === id) {
                return {...f, selected: bool}; // ***
            }
            return f;
        });
    });
};

But that's not what you're asking about, just something else to fix. :-)

The reason you see the console.log executed twice after handleCheck is that the component has to be re-rendered (for the change), and there are two visible fruits, so you see two calls to your FruitOption component function. There are two reasons for this:

  1. handleChange changes every time your Example component function is called, so FruitOption sees a new prop every time; and

  2. FruitOption doesn't avoid re-rendering when its props don't change, so even once you've fixed #1, you'd still see two console.log calls; and

Separately, there's no key on the FruitOption elements, which can cause rendering issues. Always include a meaningful key when rendering elements in an array. (Don't just use the index, it's problematic; but your fruit objects have an id, which is perfect.)

To fix it:

  1. Memoize handleChange so that it's not recreated every time, probably via useCallback, and

  2. Use React.memo so that FruitOption doesn't get called if its props don't change, and

  3. Add a meaningful key to the FruitOption elements in Example

Taking those and the handleChange fix above and putting them all together:

const Example = () => {
    const [fruits, setFruits] = React.useState([
        { id: 1, name: 'apple', visible: true, selected: false },
        { id: 2, name: 'banana', visible: false, selected: false },
        { id: 3, name: 'orange', visible: true, selected: false }
    ]);

    const visibleFruits = React.useMemo(() => {
        return fruits.filter((f) => f.visible);
    }, [fruits]);

    const handleCheck = React.useCallback(
        (bool, id) => {
            setFruits((prev) => {
                return prev.map((f) => {
                    if (f.id === id) {
                        return {...f, selected: bool}; // ***
                    }
                    return f;
                });
            });
        },
        []  // *** No dependencies since all it uses is `setFruits`, which is
            // stable for the lifetime of the component
    );

    return (
        <div>
            {visibleFruits.map((fruit) => {
                // *** Note the key
                return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
            })}
        </div>
    );
}

// *** `React.memo` will compare the props and skip the call if they're the same, reusing
// the previous call's result.
const FruitOption = React.memo(({ fruit, handleCheck }) => {
    console.log(`Rendering fruit ${fruit.id}`);
    return (
        <div key={fruit.id}>
            <input
                checked={fruit.selected}
                onChange={(e) => handleCheck(e.target.checked, fruit.id)}
                type='checkbox'
            />
            <label>{fruit.name}</label>
        </div>
    );
});

ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

As you can see, with all that in place, only the changed fruit is re-rendered.

Re React.memo: For components with more complicated requirements, you can provide a function as a second argument that determines whether the two sets of props are "the same" for rendering purposes. By default, React.memo just does a shallow equality comparison, which is often sufficient.

  • Related