Filtering react component state causes infinite render loop

Viewed 428

I'm retrieving some data from an API using useEffect, and I want to be able to filter that returned data using a prop being fed into the component from its parent.

I'm trying to filter the state after it is set by useEffect, however it looks like the component is going into an infinite render loop.

What do I need to do to prevent this?

export default function HomeJobList(props: Props): ReactElement {
    const [listings, setListings] = React.useState(null);
  
    useEffect(() => {
        const func = async () => {
          let res = await service.getListings();
          setListings(res);
        };
        func();
      }, []);
  
    if (props.searchTerm && listings) {
      let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
      setListings(filtered);
    }

  
    return (
      <>
        <div>do stuff</div>
      </>
    );
  }
  

I understand that the use of the setListing function is then causing a rerender after the filtering, which then causes another setListing call. But what's the best way to break this loop?

Should I just have another state value that maintains the last searchTerm used to filter and check against that before filtering?

Or is there a better way?

4 Answers

It's an indinite loop because every time you filter, you set it as a state variable, which causes re-rendering and filtering & setting the variable again - thus a loop.

I suggest you do it all in one place (your useEffect is a good place for that, because it only executes once.

    useEffect(() => {
        (async () => {
          const res = await service.getListings();
          const filtered = res.filter((x) => x.positionTitle.includes(props.searchTerm));
          setListings(filtered);
        })();
      }, []);

When the state changes that trigger a re-render, that's why you have an infinite loop; what you have to do is to wrap your filtering in a useEffectthat that depends on the searchTerm prop, like this:

import React, { useEffect } from 'react';

export default function HomeJobList() {
  const [listings, setListings] = React.useState(null);

  useEffect(() => {
    const func = async () => {
      let res = await service.getListings();
      setListings(res);
    };
    func();
  }, []);

  useEffect(() => {
    if (listings) {
      let filtered = listings.filter(x =>
        x.positionTitle.includes(props.searchTerm)
      );
      setListings(filtered);
    }
  }, [props.searchTerm]);

  return (
    <>
      <div>do stuff</div>
    </>
  );
}

You need to create a function that's called inside the JSX you're returning.

Actually, you'll need to render the component every time the Props objects changes. That's achieved by calling the function in the JSX code.

Example:

Function:

const filteredListings = () => {
    if (props.searchTerm && listings) {
        let filtered = listings.filter((x) => {
            x.positionTitle.includes(props.searchTerm));
        }
        return filtered;
    }
}

Return Statement:

return (
    <ul>
        {
            filteredListings().map((listing) =>
                <li>{listing.title}</li> 
            );
        }
    </ul>
);

What you need is a useEffect which does the filtering when props changes.

Replace this

 if (props.searchTerm && listings) {
      let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
      setListings(filtered);
    }

with

useEffect(()=>{
   if (props.searchTerm) {
      setListings(prevListing => {
          return prevListing.filter((x) => x.positionTitle.includes(props.searchTerm))
      });            
    }
}, [props.searchTerm] )
Related