Batch hook requests and wait for them all to finish in React

Viewed 25

I am having troubles figuring out how to wait for a series of requests to finish sent through an API hook in React.

My fetch hook:

export default function useFetch<T>(endpoint: string, config: RequestInit) {
  const [data, setData] = useState<T | undefined>();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");

  // Set headers, body, auth etc.

  const fetchData = async ({ body }: RequestConfig = {}) => {
    setIsLoading(true);
    try {
      const response = await fetch(`${BASE_URL}${endpoint}`, config);
      if (response.ok) {
        const responseData = await response.json().catch((_) => _);
        setError("");
        setData(responseData);
        setIsLoading(false);
      } else {
        setIsLoading(false);
        // Handle errors
      }
    } catch (_) {
      setError("Could not contact the server. Please try again later.");
      setIsLoading(false);
    }
  };

  const state: ResponseState<T> = { data, isLoading, error };

  return [state, fetchData] as const;
}

In the app I have a list of items and the backend only takes single requests at a time, so if I update, say five, items I will send five requests. I want to wait for all the requests to finish, so I can tell the user that the updates are successful (e.g. in the form of a toast).

Before I made my fetch hook, I used promises and then it worked great with Promise.all().

Edit:

Now I have the project container that sends the API requests:

function ProjectContainer() {
  const { projectId } = useParams();
  const [ state, getProject] = useFetch<IProject>();
  const [{ data: hasUpdated, isLoading, error }, attachMaterial] =
    useFetch<IProject>();
  const [{ data: hasUpdated }, detachMaterial] = useFetch<IProject>();
  const [openToast, setOpenToast] = useState(false);

  useEffect(() => {
    getProject(`projects/${projectId}`);
  }, []);


  // The backend only has attach and detach, so if I want to make an update to a material for a project, I have to delete (detach) the material and attach a new one
  const attachNewMaterialToProject = (materialToAttach: IProjectMaterial) => {
    attachMaterial(`projects/${projectId}/${materialToAttach.materialId!}`, {
      body: materialToAttach,
    });
  };

  const detachMaterialFromProject = (projectMaterialId: number) =>
    detachMaterial(`projects/${projectId}/${projectMaterialId}`);

  return (
    <>
      <ProjectMaterials
        attachMaterial={attachNewMaterialToProject}
        detachMaterial={detachMaterialFromProject}
      />
      <Toast
        open={(hasUpdated && !isLoading) || openToast}
        onClose={() => setOpenToast(false)}
        type={error ? "error" : "success"}
        text={error || "Save successful"}
      />
    </>
  );
}

export default ProjectContainer;

The ProjectMaterials component finds the updated fields and calls attach/detach in the parent:

function ProjectMaterials({
  // Other props
  attachMaterial,
  detachMaterial,
}: ProjectMaterialsProps) {
  const { control, handleSubmit, formState } =
    useForm<ProjectMaterialFormValues>({
      defaultValues: { materialBars: initialMaterialBars },
    });

  const { fields, append } = useFieldArray({
    control,
    name: "materialBars",
  });

  const dirtyFields = formState.dirtyFields.materialBars;

  const saveAllMaterials = async (dirtyFields) => {
    dirtyFields?.forEach((dirtyProjectMaterial) => {
      // Check for dirty fields and then call attachMaterial in the parent for each dirty item in the list
      if (dirtyProjectMaterial.id) {
        detachMaterial(dirtyProjectMaterial.id);
      }
      attachMaterial(dirtyProjectMaterial);
    });
  };

  const addMaterial = (): void => {
    append(defaultMaterialBar);
  };

  return (
    <>
      {fields.map(({ id }, index) => (
        <MaterialBar key={id} index={index} control={control} />
      ))}
      <Button onClick={addMaterial}>ADD MATERIAL</Button>
      <Button onClick={handleSubmit(saveAllMaterials)}>SAVE ALL</Button>
    </>
  );
}

export default ProjectMaterials;

In the parent I have tried hasUpdated && !isLoading. This is always true after all the requests have finished and thus the toast will never disappear.

So, using a hook API service, is there a way to wait for several requests of the same type to finish, so I can run some code (here show a toast) when I am sure all requests finished successfully (or show an error if not)?

0 Answers
Related