简体   繁体   中英

ReactJS : useMemo hook fixed my infinite rerender issue instead of useEffect

Without useMemo: infinite rerendering component

I had an issue with the code below compiling successfully but rerendering the component infinite times and I get this error in console: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

import { Box, Flex } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import Section, { SectionHeading } from "../../components/UI/Section";

import FaqList from "../../data/FaqList";
import FAQFilterBtn from "./FAQFilterBtn";
import FAQItems from "./FAQItems";

const allFaqItems = Object.values(FaqList).map((elem) => elem.content);

const allFaq = allFaqItems.map((elem, index) => (
  <Box key={index}>
    <FAQItems items={elem} faqHeading={Object.values(FaqList)[index].heading} />
  </Box>
));

const FAQSection = () => {
  const [displayList, setDisplayList] = useState(allFaq);
  const [activeFilter, setActiveFilter] = useState("All");

  const filteredAllFaq = 
      allFaqItems.map((elem, index) => {
        const faq =
          activeFilter === Object.values(FaqList)[index].heading ||
          activeFilter === "All" ? (
            elem.length ? (
              <FAQItems
                items={elem}
                faqHeading={Object.values(FaqList)[index].heading}
              />
            ) : (
              ""
            )
          ) : (
            ""
          );
        return <Box key={index}>{faq}</Box>;
      });

  const changeFilter = (filter: string) => {
    setActiveFilter(filter);
  };

  useEffect(() => {
    setDisplayList(filteredAllFaq);
  }, [activeFilter, filteredAllFaq]);

 
  console.log(activeFilter);

  return (
    <Section id="#faq-section">
      <>
        <Flex w="full" justify="space-between">
          <SectionHeading mb={0}>Frequently asked questions</SectionHeading>
        </Flex>

        <Flex m={4} mb={-4} gap={6}>
          <FAQFilterBtn
            name="All"
            active={activeFilter}
            setFilter={changeFilter}
          />
          {FaqList.map((e, index) => (
            <FAQFilterBtn
              key={index}
              name={e.heading}
              active={activeFilter}
              setFilter={changeFilter}
            />
          ))}
        </Flex>

        {displayList}
      </>
    </Section>
  );
};

export default FAQSection;

So I tried using the useEffect hook with a dependency on the changing filter (activeFilter) which would cause the component to rerender only one time but it doesn't. I tried using the useCallback hook since setStates are asynchronous but it didn't help.



With useMemo: issue seems "fixed"

Then I thought it had something to do with the filteredAllFaq being an array.map() which might be an "expensive/high load function" so I decided to go with useMemo hook which seemed to fixed the issue. Code below:

  const filteredAllFaq = useMemo(() => allFaqItems.map((elem, index) => {
    const faq =
      activeFilter === Object.values(FaqList)[index].heading ||
      activeFilter === "All" ? (
        elem.length ? (
          <FAQItems
            items={elem}
            faqHeading={Object.values(FaqList)[index].heading}
          />
        ) : (
          ""
        )
      ) : (
        ""
      );
    return <Box key={index}>{faq}</Box>;
  }), [activeFilter]);

  const changeFilter = (filter: string) => {
    setActiveFilter(filter);
  };

  useEffect(() => {
    setDisplayList(filteredAllFaq);
  }, [activeFilter, filteredAllFaq]);

  console.log(activeFilter);

Even though it fixed the rerendering issue, I feel like I'm using it the wrong way and the whole component could be done much better.

There's also a small issue after my "fix" because it seems to render/console.log(activeFilter) exactly 4 times at mount and whenever activeFilter changes. I was expecting it to render only once.

I'm new to React and have never used useMemo before. I tried searching for a solution but I don't even know where my problem is. Any advice is very much appreciated. Thank you

Instead of defining filteredAllFaq as a const in the function body, you can store its value in a useState hook. The displayList state hook is useless and causes the problem. So replace that with

const [filteredAllFaq, setFilteredAllFaq] = useState(allFaq);

When you want to change filteredAllFaq value, do it in the useEffect hook like this (not in the function body):

 useEffect(() => {
    const newFilteredAllFaq = allFaqItems.map((elem, index) => {
      const faq =
        activeFilter === Object.values(FaqList)[index].heading ||
          activeFilter === "All" ? (
          elem.length ? (
            <FAQItems
              items={elem}
              faqHeading={Object.values(FaqList)[index].heading}
            />
          ) : (
            ""
          )
        ) : (
          ""
        );
      return <Box key={index}>{faq}</Box>;
    });
    
    setFilteredAllFaq(newFilteredAllFaq);
  }, [activeFilter]);

Consider that filteredAllFaq dependency of useEffect has been removed and activeFilter dependency is enough for updating filteredAllFaq state.


As for the case that console.log is executed four times. These extra rerenders could be the result of reactStrictMode which is wrapped around the main project component (usually located in index.js file). If you remove that wrapper, extra rerenders will be stopped.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM