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)?