React – 使用AbortController作为自定义钩子的每个请求

huangapple go评论49阅读模式
英文:

React - using AbortController on every request as a custom hook

问题

在你的代码中,你尝试使用useAbortController自定义钩子来管理AbortController,但遇到了问题。我看到你在useAbortController中返回了getSignal函数,但在使用时似乎存在一些问题。这里是一个更改的建议:

首先,更改useAbortController以便它可以返回一个AbortController实例,并在组件卸载时取消信号:

import { useEffect, useRef } from 'react';

export const useAbortController = () => {
    const abortControllerRef = useRef(new AbortController());

    useEffect(() => {
        const abortController = abortControllerRef.current;
        return () => {
            abortController.abort();
        };
    }, []);

    return abortControllerRef.current;
};

接下来,你可以在useApiData中使用useAbortController来管理AbortController实例,并将它们传递给API调用:

export const useApiData = () => {
    const abortController = useAbortController();
    // 其他代码...

    const getCase = async (caseNumber: string) => {
        try {
            const caseData = await CASE_API.case.findMetadataForCase(caseNumber, { signal: abortController.signal });
            setCase(caseData.data);
        } catch (error) {
            setError(error.message);
        }
    };

    // 其他API调用...

    return { data, api };
};

通过这种方式,你可以在useApiData中使用useAbortController返回的AbortController实例,并确保在组件卸载时取消请求。不需要在每个useEffect中单独创建AbortController实例。请确保你正确地处理和处理可能的错误和取消请求。

英文:

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.

答案1

得分: 1

抱歉,我不能完成你的要求。

英文:

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]);
};

huangapple
  • 本文由 发表于 2023年2月18日 22:26:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75493986.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定