-
-
Save kentcdodds/b36572b6e9227207e6c71fd80e63f3b4 to your computer and use it in GitHub Desktop.
function useAbortController() { | |
const abortControllerRef = React.useRef() | |
const getAbortController = React.useCallback(() => { | |
if (!abortControllerRef.current) { | |
abortControllerRef.current = new AbortController() | |
} | |
return abortControllerRef.current | |
}, []) | |
React.useEffect(() => { | |
return () => getAbortController().abort() | |
}, [getAbortController]) | |
const getSignal = React.useCallback(() => getAbortController().signal, [ | |
getAbortController, | |
]) | |
return getSignal | |
} |
Solid 👍👍
Hello folks, could someone kindly show me how this works in practise?
I'm currently doing this with a ref, this looks more elegant though
Whoever found above gist first before Kent's own tweet, use the above hook like below. Directly copied from that tweet.
const getSignal = useAbortController()
// in an effect
fetch('/thing', {signal: getSignal()})
I use it this way, but it raises error: Failed to execute 'fetch' on 'Window': The user aborted a request.
const getSignal = useAbortController();
const [data, setData] = useState();
const doFetch = async () => {
const signal = getSignal();
const response = await fetch("https://codesandbox.io", { signal });
const result = await response.body;
setData(result);
};
useEffect(() => {
doFetch()
}, []);
How to solve?
How do you manually call abort()
from this?
Here is my manually controlled abortController I use to allow users to cancel an upload in React Native
function useAbortController() {
const abortControllerRef = useRef();
const getAbortController = useCallback(() => {
console.log('getAbortController', abortControllerRef.current);
if (!abortControllerRef.current) {
abortControllerRef.current = new AbortController();
}
return abortControllerRef.current;
}, []);
const abortSignal = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null; // Resets it for next time
}
}, []);
const getSignal = useCallback(
() => getAbortController().signal,
[getAbortController]
);
return { getSignal, abortSignal };
}
Inspired by @charlestbell
- Leave control to the user of the hook to
abort()
anytime they want, not just on unmount. - Returns a (memoized) object that resembles the AbortController API:
{ abort, signal }
instead of thegetSignal
function. - Strict
null
checks onabortCtrlRef.current
instead offalsey
for perf (just because).
function useAbortController() {
const abortCtrlRef = useRef( null );
const getAbortController = useCallback(
() => {
if ( abortCtrlRef.current === null ) abortCtrlRef.current = new AbortController();
return abortCtrlRef.current;
},
[]
);
const abort = useCallback(
() => {
if ( abortCtrlRef.current !== null ) {
abortCtrlRef.current.abort();
abortCtrlRef.current = null;
}
},
[]
);
return useMemo(
() => ({
get signal() { return getAbortController().signal; },
abort,
}),
[ getAbortController, abort ]
);
}
Usage:
function MyComponent( props ) {
// { abort, signal } just like a real AbortController
const { abort, signal } = useAbortController();
// Some useEffect or other thing you need to be able to abort
useEffect(() => fetch( url, { signal } ), [ signal ]);
// The user of the hook can abort whenever they please.
useEffect(() => () => abort(), [ abort ]);
}
Ia had same implementation in typescript. Only difference if that I'm aborting with reason. I mostly use this in cases when I need to stop a flow on re-render.
import React from "react"
export function useAbortController() {
const abortControllerRef = React.useRef<AbortController | undefined>()
const getAbortController = React.useCallback(() => {
abortControllerRef.current =
abortControllerRef.current && !abortControllerRef.current.signal.aborted
? abortControllerRef.current
: new AbortController()
return abortControllerRef.current
}, [])
const getSignal = React.useCallback(() => {
return getAbortController().signal
}, [getAbortController])
React.useEffect(() => {
return () => {
getAbortController().abort("Re-render")
}
}, [getAbortController])
return getSignal
}
Thank you for engaging with me on this random piece of discussion.
The original code still creates an
AbortController
, even if it's never "used", for theuseEffect
cleanup.Removing the callback, and the unintended instantiation, this seems slightly less readable but functionally more fitting to presumed intention, with no loose ends.