OscarDev
OscarDev

Reputation: 977

UseEffect is thrown when it shouldn't be

I use a useEffect in my React code which works fine at first but writing a search engine using a useState called "search" when setting its new value through an input onChange my useEffect is executed automatically again although it is never waiting changing my useState to "search", this is my code

const [search, setSearch] = useState("");

useEffect(() => {
  pages();
}, [limitPage, page, products]);

const pages = () => {
  // eslint-disable-next-line
  getProducts(limitPage, page, search);
  getXlsProducts(100000, 1);
  const totalPages = Math.ceil(totalDocs / limitPage);
  const links = [];
  for (let i = 1; i <= totalPages; i++) {
    links[i] = i;
  }
  setNumbers(links);

  setTotalDocs(products.products?.totalDocs);
  setTotalPages(products.products?.totalPages);
  setCurrentPage(products.products?.page);
  setNextPage(products.products?.hasNextPage);
  setPreviousPage(products.products?.hasPrevPage);
};

I need to use the button that calls the handleSearch() function in order to perform the search, but as soon as a change in the input automatically occurs, my useEffect is executed:

<div>
  <input
    className="border-1 border-2 border-solid rounded border-slate-300"
    value={search}
    onChange={(e) => setSearch(e.target.value)}
  />
  <button onClick={() => handleSearch()}>
    <FiSearch />
  </button>
</div>

Here is the function to use when clicking on the button but it is never used because as soon as search changes value through the input it no longer gets executed:

const handleSearch = () => {
  setLimitPage(10);
  setPage(1);
  getProducts(limitPage, page, search);
};

How can I avoid the automatic firing of my useEffect so that I can use my search function through the button? thanks.

Upvotes: 0

Views: 264

Answers (3)

maksimr
maksimr

Reputation: 5429

useEffect(() => {
  pages();
}, [limitPage, page, products]);

Dependencies in React's hooks use shallow comparison.

As @ChrisG mentioned in the comment - "You have infinite loop". It's async loop because as you said, getProducts function request products from the server. So when you get a list of products from the server even if they semantically equal as already set you still create a new array with new reference. So useEffect triggered again because reference on products list has changed!

//🥇triggered useEffect by first run
// when we create component and call `pages()`
useEffect(() => {
  pages();
}, [limitPage, page, products]);


const pages = () => {
  //🥈Make a request to the server and set products list
  // like setProducts(response.products) this cause
  // create new array with new reference
  getProducts(limitPage, page, search);
  ...
}

//🥉products === response.products // False because these are two different objects
// so we trigger useEffect again (returns to 🥈)
useEffect(() => {
  pages();
}, [limitPage, page, products]);

Upvotes: 7

Sonam Gupta
Sonam Gupta

Reputation: 383

As far as I have understood , you want getProducts() to be called on the first render as well as whenever the state limitPage and page changes and whenever Search Button is pressed.

You already have handleSearch function for calling getProducts() that is triggering the API on the click of Search button,

const handleSearch = () => {
  setLimitPage(10);
  setPage(1);
   getProducts(limitPage, page, search);
};

In the handleSearch() as mentioned by @ChrisG in comment :- you need to take care of limitPage and page value as it will use the previous value here.

for calling API on first render and whenever the states limitPage and pagechanges, remove products dependency from useEffect and modify pages() function with the required statements:-

useEffect(() => {
   pages();
 }, [limitPage, page]);

const pages = () => {
  // eslint-disable-next-line
  getProducts(limitPage, page, search);
  getXlsProducts(100000, 1);
  const totalPages = Math.ceil(totalDocs / limitPage);
  const links = [];
  for (let i = 1; i <= totalPages; i++) {
  links[i] = i;
 }
  setNumbers(links);

};

I saw several states that you want to update on the change of products state, for that , you can have another useEffect with products dependency and a new method updating the required states:-

 useEffect(() => {
   updateStates();
 }, [products]);

const updateStates = () => {
  setTotalDocs(products.products?.totalDocs);
  setTotalPages(products.products?.totalPages);
  setCurrentPage(products.products?.page);
  setNextPage(products.products?.hasNextPage);
  setPreviousPage(products.products?.hasPrevPage);
 };
 

Upvotes: 1

Nice Books
Nice Books

Reputation: 1861

The problem is the shallow comparison as stated by @maksimr. An alternative is to use class based components with shouldComponentUpdate:

shouldComponentUpdate(nextProps, nextState) {
    // if no products in current state, update when there are products in 
    // future state, or else remain.
    if(!this.state.products.length) {
        return nextState.products.length;
    }
    // update if there's a product in current state whose id
    // is not found in the next state's products
    return this.state.products.some((prod)=>(
        !nextState.products.find((p)=> (
            prod.id == p.id 
        )
    );
}

Upvotes: 0

Related Questions