useMemo hook not working with map elements

Viewed 2758

I am using useMemo hook to render map items.I added items parameter to useMemo hook, based on items change it will render. But changing loading state and items change, Item custom component rendering twice. Am i doing any mistake on using useMemo hook, please correct me.

Home:

import React, { useState, useEffect, useMemo } from "react";
import Item from "./Item";

const array = [1];
const newArray = [4];

const Home = () => {
  const [items, setItems] = useState(array);
  const [loading, setLoading] = useState(false);
  const [dataChange, setDataChange] = useState(1);

  const renderItems = (item, index) => {
    return (
      <div key={item}>
        <Item id={item}></Item>
      </div>
    );
  };
  useEffect(() => {
    if (dataChange === 2) {
      setLoading(true);
      setTimeout(() => {
        setLoading(false);
        setItems(newArray);
      }, 3000);
    }
  }, [dataChange]);

  const memoData = useMemo(() => {
    return <div>{items.map(renderItems)}</div>;
  }, [items]);

  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <input
        onClick={() => {
          setDataChange(2);
        }}
        style={{ height: 40, width: 100, margin: 20 }}
        type="button"
        value="ChangeItem"
      ></input>
      <div>{loading ? <label>{"Loading"}</label> : <div>{memoData}</div>}</div>
    </div>
  );
};
export default React.memo(Home);

Item:

import React,{useEffect} from "react";
const Item = (props) => {
  console.log("props", props);
useEffect(() => {
// call api with props.id
 }, [props]);
  return <div>Hello world {props.id}</div>;
};
export default React.memo(Item);

Result: first time : props {id: 1}

After click : props {id: 1} props {id: 4}

2 Answers

There are a few things which are not right in the code above.

  • key should be passed to the parent element in an array iteration - in your case the renderItems should pass the key to the div element
  • you are turning off the loading state before updating the items array, switching the two setState expressions will resolve your case most of the time although setState is an async function and this is not guaranteed
  • if a constant or a function is not tightly coupled to the component's state it is always best to extract it outside the component as is the case with renderItems

Here's why there is one more console.log executed enter image description here

  • also should keep in mind that memoization takes time and you would want to keep it as efficient as possible hence you can totally skip the useMemo with a React.memo component which takes care of the array because it is kept in the state and it's reference won't change on rerender if the state remains the same
    const array = [1];
    const newArray = [4];
    
    const Home = () => {
      const [items, setItems] = useState(array);
      const [loading, setLoading] = useState(false);
      const [dataChange, setDataChange] = useState(1);
    
      useEffect(() => {
        if (dataChange === 2) {
          setLoading(true);
          setTimeout(() => {
            setItems(newArray);
            setLoading(false);
          }, 3000);
        }
      }, [dataChange]);
    
      return (
        <div style={{ display: "flex", flexDirection: "column" }}>
          <input
            onClick={() => {
              setDataChange(2);
            }}
            style={{ height: 40, width: 100, margin: 20 }}
            type="button"
            value="ChangeItem"
          ></input>
          <div>
            {loading ? <label>{"Loading"}</label> : <ItemsMemo items={items} />}
          </div>
        </div>
      );
    };
    
    const renderItems = (item) => {
      return (
        <span key={item} id={item}>
          {item}
        </span>
      );
    };
    
    const Items = ({ items }) => {
      console.log({ props: items[0] });
    
      return (
        <div>
          Hello world <span>{items.map(renderItems)}</span>
        </div>
      );
    };
    
    const ItemsMemo = React.memo(Items);

UPDATE

This codesandbox shows that useMemo gets called only when the items value changes as it is supposed to do.

enter image description here

useCustomHook:

import { useEffect, useRef } from "react"

export default function useUpdateEffect(callback, dependencies) {
  const firstRenderRef = useRef(true)

  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false
      return
    }
    return callback()
  }, dependencies)
}

Create these custom hooks in your project and use them. It will prevent your first calling issue.

Related