RMCS
RMCS

Reputation: 393

How do i properly use useEffect for a async fetch call with react? react-hooks/exhaustive-deps

Hi i'm facing a problem with the useEffect hook in React. The code below works like it should but es-lint suggests that i need to provide dependencies in the dependency array from the useEffect.

Working code with // eslint-disable-next-line react-hooks/exhaustive-deps

export default function UsersList() {
   const [users, setUsers] = useState<User[]>([]);
   
   const { setError } = useContext(errorContext);
   const { isLoading, setIsLoading } = useContext(globalContext);
   
   useEffect(() => {
       if (users.length < 1) {
         fetchUsers();
       }
       // eslint-disable-next-line react-hooks/exhaustive-deps
     }, []);

     async function fetchUsers () {
       try {
         setIsLoading(true);
         const fetchedUsers = await api.getUsers();
         setUsers(fetchedUsers);
       } catch (error) {
         setError(error);
       } finally {
         setIsLoading(false);
       }
     }
}

Infinite looping code

i tried to write it like this the code triggers a infinite loop.. (because the states get constantly changed inside the function and triggers the useEffect every time because of the declared dependencies)

 useEffect(() => {
    async function fetchUsers () {
      try {
        setIsLoading(true);
        const fetchedUsers = await api.getUsers();
        setUsers(fetchedUsers);
      } catch (error) {
        setError(error);
      } finally {
        setIsLoading(false);
      }
    }

    if (users.length < 1) {
      fetchUsers();
    }
  }, [setIsLoading, setError, users]);

i also tried putting the fetchUsers() inside the dependencies array but this had no effect.

How do i properly set up a async call when the component mounts without having to use // eslint-disable-next-line react-hooks/exhaustive-deps ?

Upvotes: 3

Views: 2426

Answers (1)

brmk
brmk

Reputation: 360

Your fetchUsers function is recreating itself with every render triggering use effect. You have to keep its reference the same across renders by wrapping it with useCallback, see https://reactjs.org/docs/hooks-reference.html#usecallback

Furthermore, to ensure that we call this useEffect only once (when the first render occurs) we can use useRef to store a boolean value which will prevent useEffect from infinite loop

export default function UsersList() {
  const [users, setUsers] = useState<User[]>([]);
  
  const { setError } = useContext(errorContext);
  const { isLoading, setIsLoading } = useContext(globalContext);

  const fetchUsers = useCallback(async function () {
    try {
      setIsLoading(true);
      const fetchedUsers = await api.getUsers();
      setUsers(fetchedUsers);
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  }, [setIsLoading, setUsers, setError]);

  // Added a ref here to ensure that we call this function only once in initial render
  // If you need to refetch the users on error, just call fetchUsers
  const isFetchedRef = useRef(false);
  useEffect(() => {
    if (!isFetchedRef.current) {
      isFetchedRef.current = true;
      fetchUsers();
    }
  }, [isLoading, fetchUsers]);
}

Upvotes: 2

Related Questions