JoaoP_L
JoaoP_L

Reputation: 39

Maximum depth exceeded while using useEffect

I am trying to implement a simple search algorithm for my products CRUD. The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button. However, the way I tried to do it was like this:

function filterProducts (productName, productList) {
  const queryProducts = productList.filter((prod)=> {
    return prod.title === productName;
  });
  return queryProducts;
}

function HomePage () {
  const [productList, setProductList] = useState([]);
  const [popupTrigger, setPopupTrigger] = useState('');
  const [productDeleteId, setProductDeleteId] = useState('');
  const [queryString, setQueryString] = useState('');
  let history = useHistory();


  useEffect(() => {
    if (queryString.trim() === "") {
      Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
        setProductList(data.data);
      });
      return;
    }
    const queryProducts = filterProducts(queryString, productList);
    setProductList(queryProducts);
  }, [queryString, productList]);

I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me. The error is this one below:

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.

Upvotes: 2

Views: 1713

Answers (3)

Martin
Martin

Reputation: 6146

I recommend several code changes.

  1. I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
  1. I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
  1. I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
    if (query.trim() === '') {
      Axios
        .get("http://localhost:3001/api/product/get-all")
        .then((data) => { setProducts(data.data) });
    }
  }, [query]);

useEffect(
    () => setFilteredProducts(filterProducts(userInput, products)),
    [userInput, products]
);

Upvotes: 0

Shyam
Shyam

Reputation: 5497

what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.

function filterProducts (productName = '', productList = []) {
  return productName.trim().length > 0 ? productList.filter((prod)=> {
    return prod.title === productName;
  }); : productList
}

function HomePage () {
  const [productList, setProductList] = useState([]);
  const [queryString, setQueryString] = useState('');
 


  useEffect(() => {
    if (queryString.trim() === "") {
      Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
        setProductList(data.data);
      });
    }
  }, [queryString]);

  // query products is the derived state 
  const queryProducts = filterProducts(queryString, productList);

  // Now instead of using productList to render something use the queryProducts
  return (
    {queryProducts.map(() => {
      ..... 
    })}
  )

If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo

const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);

Upvotes: 2

isaacsan 123
isaacsan 123

Reputation: 1158

When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.

So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).

function filterProducts (productName, productList) {
  const queryProducts = productList.filter((prod)=> {
    return prod.title === productName;
  });
  return queryProducts;
}

function HomePage () {
  const [productList, setProductList] = useState([]);
  const [popupTrigger, setPopupTrigger] = useState('');
  const [productDeleteId, setProductDeleteId] = useState('');
  const [queryString, setQueryString] = useState('');
  let history = useHistory();


  useEffect(() => {
    if (queryString.trim() === "") {
      Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
        setProductList(data.data);
      });
      return;
    }

    setProductList(prevProductList => {
        return filterProducts(queryString, prevProductList)
    });
  }, [queryString]);

Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.

Upvotes: 0

Related Questions