Search code examples
javascriptreactjscomponentsrerenderreact-memo

React Hooks - Preventing child components from rendering


As a newbie in React, it seems that re-rendering of components is the thing not to do. Therefore, for example, if I want to create a menu following this architecture : App is parent of Menu, which have a map function which creates the MenuItem components

  • menu items come from a data source (here it's const data)
  • when I click on a MenuItem, it updates the state with the selected MenuItem value

for now it's fine, except that all the components are re-rendered (seen in the various console.log)

Here's the code :

App

import React, { useState} from "react"
import Menu from "./menu";

function App() {

  const data = ["MenuItem1", "MenuItem2", "MenuItem3", "MenuItem4", "MenuItem5", "MenuItem6"]

  const [selectedItem, setMenuItem] = useState(null)

  const handleMenuItem = (menuItem) => {
    setMenuItem(menuItem)
  }

  return (
    <div className="App">
      <Menu items = {data} handleMenuItem = {handleMenuItem}></Menu>
      <div>{selectedItem}</div>
    </div>
  );
}

export default App;

Menu

import React from "react";
import MenuItem from "./menuItem";

const Menu = (props) => {

    return (
        <>
            {props.items.map((item, index) => {
                return <MenuItem key = {index} handleMenuItem = {props.handleMenuItem} value = {item}></MenuItem>
            })
            }
            {console.log("menuItem")}
        </>
    )
};

export default React.memo(Menu);

MenuItem

import React from "react";

const MenuItem = (props) => {

    return (
        <>
        <div onClick={() => props.handleMenuItem(props.value)}>
            <p>{props.value}</p>
        </div>
        {console.log("render du MenuItem")}
        </>
    )

};

export default React.memo(MenuItem);

as you might see, I've used the React.memo in the end of MenuItem but it does not work, as well as the PureComponent

If someone has an idea, that'd be great to have some advice.

Have a great day


Solution

  • There's a lot to unpack here so let's get started.

    The way hooks are designed to prevent re-rendering components unnecessarily is by making sure you use the same instance of any unchanged variables, most specifically for object, functions, and arrays. I say that because string, number, and boolean equality is simple 'abc' === 'abc' resolves to true, but [] === [] would be false, as those are two DIFFERENT empty arrays being compared, and equality in JS for objects and functions and arrays only returns true when the two sides being compared are the exact same item.

    That said, react provides ways to cache values and only update them (by creating new instances) when they need to be updated (because their dependencies change). Let's start with your app.js

    import React, {useState, useCallback} from "react"
    import Menu from "./menu";
    
    // move this out of the function so that a new copy isn't created every time
    // the App component re-renders
    
    const data = ["MenuItem1", "MenuItem2", "MenuItem3", "MenuItem4", "MenuItem5", "MenuItem6"]
    
    function App() {
    
      const [selectedItem, setMenuItem] = useState(null);
    
      // cache this with useCallback.  The second parameter (the dependency
      // array) is an empty array because there are no items that, should they
      // change, we should create a new copy.  That is to say we should never
      // need to make a new copy because we have no dependencies that could
      // change.  This will now be the same instance of the same function each 
      // re-render.
      const handleMenuItem = useCallback((menuItem) => setMenuItem(menuItem), []);
    
      return (
        <div className="App">
          <Menu items={data} handleMenuItem={handleMenuItem}></Menu>
          <div>{selectedItem}</div>
        </div>
      );
    }
    
    export default App;
    

    Previously, handleMenuItem was set to a new copy of that function every time the App component was re-rendered, and data was also set to a new array (with the same entries) on each re-render. This would cause the child component (Menu) to re-render each time App was re-rendered. We don't want that. We only want child components to re-render if ABSOLUTELY necessary.

    Next is the Menu component. There are pretty much no changes here, although I would urge you not to put spaces around your = within your JSX (key={index} not key = {index}.

    import React from "react";
    import MenuItem from "./menuItem";
    
    const Menu = (props) => {
    
        return (
            <>
                {props.items.map((item, index) => {
                    return <MenuItem key={index} handleMenuItem={props.handleMenuItem} value={item}/>
                })
                }
                {console.log("menuItem")}
            </>
        )
    };
    
    export default React.memo(Menu);
    

    For MenuItem, let's cache that click handler.

    import React from "react";
    
    const MenuItem = (props) => {
      // cache this function
      const handleClick = useCallback(() => props.handleMenuItem(props.value), [props.value]);
    
        return (
            <>
            <div onClick={handleClick}>
                <p>{props.value}</p>
            </div>
            {console.log("render du MenuItem")}
            </>
        )
    
    };
    
    export default React.memo(MenuItem);