I have a context provider in my app:
export const FormContext = createContext<IFormContext | null>(null);
function FormProvider({ caseNumber, children, ...props }: PropsWithChildren<IFormProviderContextProps>) {
const {
data: { caseNumber, taxDocuments, roles },
api,
} = useApiData();
const [error, setError] = useState<string>(null);
const [searchParams, setSearchParams] = useSearchParams();
const activeStep = searchParams.get("step");
const setActiveStep = useCallback((x: number) => {
searchParams.delete("steg");
setSearchParams([...searchParams.entries(), ["step", Object.keys(STEPS).find((k) => STEPS[k] === x)]]);
}, []);
useEffect(() => {
const abortController = new AbortController();
if (case) api.getPersons(case, abortController.signal).catch((error) => setError(error.message));
return () => {
abortController.abort();
};
}, [case]);
useEffect(() => {
const abortController = new AbortController();
if (activeStep === Stepper.INCOME) {
api.getTaxDocuments(abortController.signal).catch((error) => setError(error.message));
}
return () => {
abortController.abort();
};
}, [activeStep]);
useEffect(() => {
const abortController = new AbortController();
api.getCase(caseNumber, abortController.signal).catch((error) => setError(error.message));
}
return () => {
abortController.abort();
};
}, []);
return (
<FormContex.Provider value={{ taxDocuments, case, roles, activeStep, setActiveStep, error, ...props }}>
{children}
</FormContex.Provider>
);
}
I am using this FormProvider as a wrapper for my FormPage:
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/:caseNumber" element={<FormWrapper />} />
<Route path="/" element={<div>Hello world</div>} />
</Routes>
</BrowserRouter>
</React.StrictMode>
function FormWrapper() {
const { caseNumber } = useParams<{ caseNumber?: string }>();
return (
<FormProvider caseNumber={caseNumber}>
<FormPage />
</FormProvider>
);
}
In my FormPage I display components based on the activeStep that I get from FromProvider
export default function FormWrapper({ activeStep, ...props }: FormWrapperProps) {
const renderForm = useMemo(() => {
switch (activeStep) {
case Stepper.TIMELINE:
return <Timeline {...props} />;
case Stepper.INCOME:
return <Income {...props} />;
case Stepper.RESIDENCY:
return <Residency {...props} />;
case Stepper.SUMMARY:
return <Summary {...props} />;
default:
return <Timeline {...props} />;
}
}, [activeStep]);
return <Suspense fallback={<Loader size="3xlarge" title="loading..." />}>{renderForm}</Suspense>;
}
What I would like to do is to implement an abort controller if component gets unmounted to stop the fetch request and state update. I have tried that with implementing it inside useEffect
functions of the FormProvider
. But, that is repetitive and would like to make some kind of function or a hook that would set the abort controller to every request. I am not sure how to do that with the current setup, where I have my api calls defined in useApiData()
hook which looks like this:
export const useApiData = () => {
const [case, setCase] = useState<CaseDto>(null);
const [taxDocuments, setTaxDocuments] = useState<TaxDocumentsResponse[]>([]);
const [roles, setRoles] = useState<IRoleUi[]>([]);
const getCase = async (caseNumber: string, signal?: AbortSignal) => {
const case = await CASE_API.case.findMetadataForCase(caseNumber, { signal });
setCase(case.data);
};
const getPersons = async (case: CaseDto, signal?: AbortSignal) => {
const personPromises = case.roles.map((role) =>
PERSON_API.information.getPersonPost(
{ id: role.id },
{ signal }
)
);
const [...persons] = await Promise.all([...personPromises]);
const roles = persons.map((person) => {
const role = case.roles.find((role) => role.id === person.data.id);
if (!role) throw new Error(PERSON_NOT_FOUND);
return { ...role, ...person.data };
});
setRoles(roles);
};
const getTaxDocuments = async (signal?: AbortSignal) => {
const taxDocumentsDtoPromises = [getFullYear() - 1, getFullYear() - 2, getFullYear() - 3].map((year) =>
TAX_API.integration.getTaxDocument(
{
year: year.toString(),
filter: "",
personId: "123",
},
{ signal }
)
);
const [taxDocument1, taxDocument2, taxDocument3] = await Promise.all([...taxDocumentsDtoPromises]);
setTaxDocuments([taxDocument1.data, taxDocument2.data, taxDocument3.data]);
};
const api = {
getCase,
getPersons,
getTaxDocuments,
};
const data = {
case,
roles,
taxDocuments,
};
return { data, api };
}
As I said I would like to be able to call api without having to define abort controller in every useEffect
hook, but I am not sure how to achieve some like this for example:
apiWithAbortController.getCase(caseNumber).catch((error) => setError(error.message))}
I have tried with using a custom hook like this:
export const useAbortController = () => {
const abortControllerRef = useRef<AbortController>();
useEffect(() => {
return () => abortControllerRef.current?.abort();
}, []);
const getSignal = useCallback(() => {
if (!abortControllerRef.current) {
abortControllerRef.current = new AbortController();
}
return abortControllerRef.current.signal;
}, []);
return getSignal;
};
That I was using like this in my useApiData
:
const signalAbort = useAbortController();
const getCase = async (caseNumber: string) => {
const case = await CASE_API.case.findMetadataForCase(caseNumber, { signal: signalAbort() });
setCase(case.data);
};
But, that didn't work, with that setup none of the fetch calls were made.
There is no way to cancel all the network requests globallly at once. you have to attach an abort controller to each fetch calls.
import { useEffect } from 'react';
export const useAbortController = (fetcher,args,dependencies) => {
useEffect(() => {
const abortController = new AbortController();
const signal = abortController.signal;
// fetch here. write a reusable form based on your api function
fetcher(...args,{signal})
// you could also setTimeout and maybe after 2 seconds call abortController.abort()
return () => abortController.abort();
}, [...dependencies]);
};