lrefopam
lrefopam

Reputation: 531

ts/react - fetch in useEffect gets called multiple times

in my functional component I want to fetch data once the component mounts. But unfortunately, the request gets fired three times, until it stops. Can you tell me why?

 const [rows, setRows] = useState<any[]>([]);
 const [tableReady, setTableReady] = useState<boolean>(false);
 const [data, setData] = useState<any[]>([]);


const getData = async () => {
      const user = await Amplify.Auth.currentAuthenticatedUser();
      const token = user.signInUserSession.idToken.jwtToken;
      const apiurl = 'xxx';

      fetch(apiurl, {
           method: 'GET',
           headers: {
                'Authorization': token
           }
      })
           .then(res => res.json())
           .then((result) => {
                setData(result);
           })
           .catch(console.log)
 }

 useEffect(() => {
      if (!tableReady) {
           getData();

           if (data.length > 0) {
                data.forEach((element, i) => {
                     const convertedId: number = +element.id;
                     setRows(rows => [...rows, (createData(convertedId, element.user))]);
                });
                setTableReady(true);
           }
      }
 }, []);

 return (
      <div className={classes.root}>
           <MUIDataTable
                title={""}
                data={rows}
                columns={columns}
           />
      </div>
 );

I updated my question due to the comment.

Upvotes: 2

Views: 691

Answers (1)

Drew Reese
Drew Reese

Reputation: 203146

The useEffect is missing a dependency array, so its callback is invoked every time the component renders.

Solution

Add a dependency array.

useEffect(() => {
  if (!tableReady) {
    getData();

    if (data.length > 0) {
      data.forEach((element, i) => {
        const convertedId: number = +element.id;
        rows.push(convertedId);
      });
      setTableReady(true);
    }
  }
}, []); // <-- dependency array

An empty dependency array will run the effect once when the component mounts. If you want it to ran when any specific value(s) update then add these to the dependency array.

See Conditionally firing an effect

Edit

It doesn't appear there is any need to store a data state since it's used to populate the rows state. Since React state updates are asynchronously processed, and useEffect callbacks are 100% synchronous, when you call getData and don't wait for the data to populate, then the rest of the effect callback is using the initially empty data array.

I suggest returning the fetch request from getData and just process the response data directly into your rows state.

const getData = async () => {
  const user = await Amplify.Auth.currentAuthenticatedUser();
  const token = user.signInUserSession.idToken.jwtToken;
  const apiurl = 'xxx';

  return fetch(apiurl, {
    method: 'GET',
    headers: {
      'Authorization': token
    }
  });  
}

useEffect(() => {
  if (!tableReady) {
    getData()
      .then(res => res.json())
      .then(data => {
        if (data.length) {
          setRows(data.map(element => createData(+element.id, element.user)))
        }
      })
      .catch(console.error)
      .finally(() => setTableReady(true));
  }
}, []);

Upvotes: 4

Related Questions