Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Created April 3, 2020 23:32
Show Gist options
  • Save kentcdodds/b36572b6e9227207e6c71fd80e63f3b4 to your computer and use it in GitHub Desktop.
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
}
@kentcdodds
Copy link
Author

Solid 👍👍

@PerryRylance
Copy link

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

@nzkks
Copy link

nzkks commented Jul 21, 2023

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()})

@webgodo
Copy link

webgodo commented Aug 3, 2023

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?

@charlestbell
Copy link

How do you manually call abort() from this?

@charlestbell
Copy link

charlestbell commented Aug 8, 2023

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

@rejhgadellaa
Copy link

rejhgadellaa commented Jul 25, 2024

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 the getSignal function.
  • Strict null checks on abortCtrlRef.current instead of falsey 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 ]);

}

@sagarpanchal
Copy link

sagarpanchal commented Oct 28, 2024

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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment