useMemo hook not working with map elements
Asked Answered
R

2

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}

Rena answered 24/9, 2021 at 10:35 Comment(6)
may you create a sandbox?Rajkot
You can try without useMemo.Photoelectric
@Photoelectric same result i got, with useMemo i want to restrict my render elements. Unfortunately Item component is rendering twiceRena
@Rajkot i don't know how to add it sandboxRena
Another way of improving useMemo is to use JSON.stringify(items) as the trigger. This will look the same even though references changes, as long as the content is the same, it will not run again. So basically useMemo(() => {}, [JSON.stringify(items)]);Charlean
i tried with JSON.stringfy(items) still same resultRena
M
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

Muriel answered 27/9, 2021 at 7:31 Comment(9)
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.Rena
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.Muriel
actual problem is inside Item component i am calling api based on props changes, but because of this issue multiple times api calls are goingRena
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 setStates and run the code?Muriel
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 changeRena
i am not saying that it is a perfect design, i am just checking useMemo hookRena
If you want to see if useMemo works fine - just add a console.log there - see the updated answer.Muriel
I know useMemo hook calling only when data change, but useMemo code contains renderItems, renderItems contains Item component, how Item component is triggeringRena
Let us continue this discussion in chat.Muriel
L
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.

Laciniate answered 2/10, 2021 at 7:59 Comment(1)
@jadeja, thanks for the solution, but i wanted to know why useMemo hook is failing for map elementsRena

© 2022 - 2024 — McMap. All rights reserved.