Why my component is rendering 4x times even with useCallback and useEffect?

Advertisements

I’m a beginner in React and I’m trying to fetch data using Axios.

This fetching is being controlled by useState (loading, errors, getting data) and useEffect/useCallback.

My code has a console.log("hey") just to show me when the component is being rendered.

import { useCallback } from "react";
import { useState, useEffect } from "react";
import DriversList from "./components/DriversList";
import axios from "axios";

function App() {
  const [drivers, setDrivers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const apiGetDataHandler = useCallback(() => {
    setIsLoading(true);
    setError(null);

    axios
      .get("http://ergast.com/api/f1/2022/drivers.json?limit=25")
      .then((response) => {
        setIsLoading(false);
        setDrivers(response.data.MRData.DriverTable.Drivers);
      })
      .catch((error) => {
        setError(error.message);
      });
  }, []);

  useEffect(() => {
    apiGetDataHandler();
  }, [apiGetDataHandler]);

  let content = <DriversList drivers={drivers} />;
  console.log("hey");

  if (error) {
    content = <p>{error}</p>;
  }

  if (isLoading) {
    content = <p>Loading...</p>;
  }

  return (
    <>
      <section>{content}</section>
    </>
  );
}

export default App;

>Solution :

Assuming react 18 and <StrictMode>, here’s the 4 renders:

The component mounts, and that’s render #1

After the first render, the effect runs and you call setIsLoading(true) and setError(null). Since loading changed from false to true, this results in render #2. Also, the axios.get is kicked off.

Because of strict mode, react simulates unmounting/remounting the component, and runs your effect again. The calls to setIsLoading and setError won’t actually cause another render, because the new values are the same as the old values, but the effect does kick off an additional call to axios.get

Some time later, one of the axios.gets finishes. You call setIsLoading(false) and setDrivers(response.data.MRData.DriverTable.Drivers). Both of these result in changes, which are batched up into a single render #3.

Some time later, the second axios.get finishes. You call setIsLoading(false) and setDrivers(response.data.MRData.DriverTable.Drivers). The loading part doesn’t change anything, but the drivers does change, resulting in render #4.


You can eliminate 1 render by having isLoading start off as true. That way, when you call setIsLoading(true) right after mount, nothing changes.

const [isLoading, setIsLoading] = useState(true);

You can eliminate another render by having your useEffect return a teardown function which stops the fetch when the component unmounts. You could either cancel the fetch entirely via an axios abort controller, or just set a variable reminding you to not set the state when it finishes. Also, if you move the function into the effect, you won’t need to use useCallback:

useEffect(() => {
  let cancelled = false;

  setIsLoading(true);
  setError(null);

  axios
    .get("http://ergast.com/api/f1/2022/drivers.json?limit=25")
    .then((response) => {
      if (cancelled) {
        return;
      }
      setIsLoading(false);
      setDrivers(response.data.MRData.DriverTable.Drivers);
    })
    .catch((error) => {
      if (cancelled) {
        return;
      }
      setError(error.message);
    });

  return () => {
    cancelled = true;
  } 
}, []);

Leave a ReplyCancel reply