Search code examples
reactjsreact-hooksuse-effect

useEffect dependency causes infinite loop


I created a custom hook which I use in App.js

The custom hook (relevant function is fetchTasks):

export default function useFetch() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);
  const [tasks, setTasks] = useState([]);

  const fetchTasks = async (url) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error("falied!");
      }
      const data = await response.json();
      const loadedTasks = [];
      for (const taskKey in data) {
        loadedTasks.push({ id: taskKey, text: data[taskKey].text });
      }

      setTasks(loadedTasks);
    } catch (err) {
      console.log(err.message);
    }
    setLoading(false);
  };

  return {
    loading,
    setLoading,
    error,
    setError,
    fetchTasks,
    tasks,
  };
}

Then in my App.js:

function App() {
  const { loading, setLoading, error, setError, fetchTasks, tasks } =
    useFetch();

  useEffect(() => {
    console.log("fetching");
    fetchTasks(
      "https://.....firebaseio.com/tasks.json"
    );
  }, []);

My IDE suggests adding the fetchTasks function as a dependency to useEffect. But once I add it, an infinite loop is created. If I omit it from the dependencies as shown in my code, it will work as expected, but I know this is a bad practice. What should I do then?


Solution

  • Because that every time you call useFetch(). fetchTasks function will be re-created. That cause the reference to change at every render then useEffect() will detected that dependency fetchTasks is re-created and execute it again, and make the infinite loop. So you can leverage useCallback() to memoize your fetchTasks() function so the reference will remains unchanged.

    import { useCallback } from 'react'
    
    export default function useFetch() {
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState(false);
      const [tasks, setTasks] = useState([]);
    
      const fetchTasks = useCallback(
        async (url) => {
        setLoading(true);
        setError(null);
        try {
          const response = await fetch(url);
    
          if (!response.ok) {
            throw new Error("falied!");
          }
          const data = await response.json();
          const loadedTasks = [];
          for (const taskKey in data) {
            loadedTasks.push({ id: taskKey, text: data[taskKey].text });
          }
    
          setTasks(loadedTasks);
        } catch (err) {
          console.log(err.message);
        }
        setLoading(false);
      };,[])
    
      return {
        loading,
        setLoading,
        error,
        setError,
        fetchTasks,
        tasks,
      };
    }
    
    function App() {
      const { loading, setLoading, error, setError, fetchTasks, tasks } =
        useFetch();
    
      useEffect(() => {
        console.log("fetching");
        fetchTasks(
          "https://.....firebaseio.com/tasks.json"
        );
      }, [fetchTasks]);