reactjs - Object.is likhetssjekk i React-hook forårsaker flere versjoner av samme funksjon
Jeg er ikke sikker på hva den riktige løsningen er for å stoppe følgende scenario. Jeg har laget denne codesandboxen for å illustrere problemet.
Jeg har denne kroken, og her er en nedskalert versjon:
export const useAbortable = <T, R, N>(
fn: () => Generator<Promise<T>, R, N>,
options: Partial<UseAbortableOptions<N>> = {}
) => {
const resolvedOptions = {
...DefaultAbortableOptions,
...options
} as UseAbortableOptions<N>;
const { initialData, onAbort } = resolvedOptions;
const initialState = initialStateCreator<N>(initialData);
const abortController = useRef<AbortController>(new AbortController());
const counter = useRef(0);
const [state, dispatch] = useReducer(reducer, initialState);
const runnable = useMemo(
() =>
makeRunnable({
fn,
options: { ...resolvedOptions, controller: abortController.current }
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[counter.current]
);
const runner = useCallback(
(...args: UnknownArgs) => {
console.log(counter.current);
dispatch(loading);
runnable(...args)
.then(result => {
dispatch(success<N>(result));
})
.finally(() => {
console.log("heree");
counter.current++;
});
},
[runnable]
);
return runner;
};
Hooken tar en funksjon og et alternativobjekt, og siden de blir gjenskapt ved hver rendering, og hookene bruker Object.is-sammenligning, ble det laget en ny versjon av den returnerte funksjonen uansett hva jeg gjør.
Så jeg har hacket det på denne måten, for å bruke en teller:
const runnable = useMemo(
() =>
makeRunnable({
fn,
options: { ...resolvedOptions, controller: abortController.current }
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[counter.current]
);
Jeg har måttet dempe linteren for å gjøre dette mulig.
Hva linteren foreslår er dette:
const runnable = useMemo(
() => makeRunnable({ fn, options: { ...resolvedOptions, controller: abortController.current } }),
[fn, resolvedOptions],
);
Men fn og resolvedOptions forårsaker at en ny kjørbar funksjon blir opprettet hver gang.
Det er virkelig tungvint å måtte omslutte alt i useCallback, useMemo og lignende.
Jeg har sett på andre fetch-biblioteker, og de gjør andre ting som dette, som å JSON.stringify avhengighetsarrayer for å komme rundt dette problemet.
Jeg liker hooks, men Object.is-likhetssjekken ødelegger hele paradigmet.
Hva ville være den riktige måten for meg å bruke avhengighetslisten på slik at jeg ikke får en ny funksjon hver gang og samtidig holder linteren fornøyd? De to kravene ser ut til å være i konflikt med hverandre.
Svar #1
Du må merke deg at du ikke trenger å gi noen hack for å omgå tilbakekallingsproblemet
ESLint-advarsler om manglende avhengighet er der for å hjelpe brukere med å unngå feil de uvitende kan gjøre, og ikke for å håndheves.
Nå i ditt tilfelle
Hvis du ser på din useAbortable-funksjon, sender du inn en generatorfunksjon og et alternativobjekt, som begge blir opprettet ved hver re-render.
Du kan memoisere alternativene og funksjonen som sendes til useAbortable for å unngå problemer med avhengigheter
Hvis du bruker
callback-mønster for setMessages, kan du oppretteonAbortkun en gang ved å gi[]som avhengighet til useCallbackGeneratorfunksjonen avhenger av en tidsforsinkelse fra tilstanden, så du kan lage den med useCallback og angi
delaysom en avhengighet
const onAbort = useCallback(() => {
setMessages(prevMessage => (["We have aborted", ...prevMessage]));
}, []); //
const generator = useCallback(
function*() {
const outsideLoop = yield makeFetchRequest(delay, "outside");
processResult(outsideLoop);
try {
for (const request of requests) {
const result = yield makeFetchRequest(delay, `${request.toString()}`);
processResult(result);
}
} catch (err) {
if (err instanceof AbortError) {
setMessages(["Aborted"]);
return;
}
setMessages(["oh no we received an error", err.message]);
}
},
[delay, processResult]
);
const options = useMemo(() => ({ onAbort }), [onAbort]);
const { run, state, abortController, reset, counter, ...rest } = useAbortable<
Expected,
void,
Expected
>(generator, options);
Nå inne i useAbortable trenger du ikke bekymre deg for at fn eller options endres, da de bare vil endres når det absolutte nødvendig dersom vi implementerer det som ovenfor
Så du din kjørbare forekomst i useAbortable kan klart opprettes med de riktige avhengighetene
const resolvedOptions = useMemo(() => ({
...DefaultAbortableOptions,
...options
}), [options]);
const runnable = useMemo(
() =>
makeRunnable({
fn,
options: { ...resolvedOptions, controller: abortController.current }
}),
[fn, resolvedOptions]
);
Arbeidende demo
Svar #2
Problemet er med resolvedOptions og fn og ikke med runnable. Du bør omskrive runnable-avhengigheter slik lint-verktøyet foreslår og endre resolvedOptions til også å være memoized.
// I assume that `DefaultAbortableOptions` is defined outside of the `useAbortable` hook
const resolvedOptions = useMemo(() => ({
...DefaultAbortableOptions,
...options
}), [options]};
Hooken tar en funksjon og et alternativobjekt, og ettersom de blir gjenskapt ved hver render, og hookene bruker Object.is-sammenligning, opprettet det en ny versjon av den returnerte funksjonen uansett hva jeg gjorde.
Når du bruker din hook, bør du også være oppmerksom på å ikke gjenopprette ting unødvendig og bruke useMemo og useCallback for data og funksjoner definert inne i komponentene dine.
function MyComponent () {
const runnable = useCallback(() => {}, [/*let the linter auto-fill this*/])
const options = useMemo(() => ({}), [/*let the linter auto-fill this*/])
const runner = useAbortable(runnable, options)
}
På denne måten vil runner-funksjonen bare bli gjenopprettet når det virkelig er nødvendig (når de dynamiske avhengighetene til runnable og options endres).
Du må gå all-in med hooks hvis du bruker dem og virkelig pakke alt inn i dem for å få ting til å fungere uten skumle feil. Jeg personlig misliker dem på grunn av disse egenskapene.
Ekstra merknad: som React-dokumentasjonen påpeker, er det ikke garantert at useMemo kun kjører når avhengighetene endres, det kan kjøre ved hvilken som helst rendering og føre til at runner blir gjenskapt. I nåværende versjon av React skjer ikke dette, men det kan skje i fremtiden.
