6

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}

Kia Kaha
  • 1,565
  • 1
  • 17
  • 39
skyshine
  • 2,767
  • 7
  • 44
  • 84

2 Answers2

8

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

Kia Kaha
  • 1,565
  • 1
  • 17
  • 39
  • Thanks @Kia Kaha for noticing key and useMemo issues, but in real time these type of issue may arise. My question is why Item component is calling first without data change. – skyshine Sep 27 '21 at 07:43
  • You have places the console.log inside the component which means it will execute on each 'render`, right? When you tigger the `loading` state you are hiding and showing again the `Item` component which triggers `rendering` although the value haven't changed. And then the `items` value changes which triggers one more time the component to `rerender`. Hope this is more clear as I tried to notice it above also. – Kia Kaha Sep 27 '21 at 07:48
  • actual problem is inside Item component i am calling api based on props changes, but because of this issue multiple times api calls are going – skyshine Sep 27 '21 at 07:57
  • then your example is irrelevant and there's no way to address your issue based on the code above. Sounds like your design is not right but please provide a justifiable example. Also - have you tried switching the `setState`s and run the code? – Kia Kaha Sep 27 '21 at 08:51
  • if i keep setState after setting items it will work that i know but why useMemo is not taking care of items change and inside render items how Item component called without items change – skyshine Sep 27 '21 at 09:58
  • i am not saying that it is a perfect design, i am just checking useMemo hook – skyshine Sep 27 '21 at 09:59
  • If you want to see if useMemo works fine - just add a console.log there - see the updated answer. – Kia Kaha Sep 27 '21 at 13:25
  • I know useMemo hook calling only when data change, but useMemo code contains renderItems, renderItems contains Item component, how Item component is triggering – skyshine Sep 28 '21 at 06:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237593/discussion-between-kia-kaha-and-skyshine). – Kia Kaha Sep 28 '21 at 12:43
0

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.

  • @jadeja, thanks for the solution, but i wanted to know why useMemo hook is failing for map elements – skyshine Oct 11 '21 at 07:02