DEV Community

Discussion on: Avoiding Race Conditions when Fetching Data with React Hooks

Collapse
vercetti11 profile image
Octavio Vercetti • Edited on

Does not work for components that fetch based on props.
But using the AbortController object makes it work as intended.

import { useEffect, useRef, useReducer, useCallback } from "react";

function asyncReducer(state, action) {
  switch (action.type) {
    case "pending": {
      return { status: "pending", data: null, error: null };
    }
    case "resolved": {
      return { status: "resolved", data: action.data, error: null };
    }
    case "rejected": {
      return { status: "rejected", data: null, error: action.error };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

export const useFetch = url => {
  const isCurrent = useRef(false);
  const [state, dispatch] = useReducer(asyncReducer, {
    status: "idle",
    data: null,
    error: null,
  });

  useEffect(() => {
    isCurrent.current = true;
    return () => {
      // called when the component is going to unmount
      isCurrent.current = false;
    };
  }, []);

  const fetchURL = useCallback(
    controller => {
      dispatch({ type: "pending" });
      fetch(url, {
        signal: controller.signal,
      })
        .then(res => res.json())
        .then(data => {
          if (!isCurrent.current) return;
          dispatch({ type: "resolved", data });
        })
        .catch(error => {
          if (!isCurrent.current) return;
          dispatch({ type: "rejected", error });
        });
    },
    [url]
  );

  useEffect(() => {
    let controller = new AbortController();
    fetchURL(controller);
    return () => controller.abort();
  }, [fetchURL]);

  return { state, fetchURL };
};

Enter fullscreen mode Exit fullscreen mode