use-async-with-cancel.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import * as React from 'react'
  2. import useSafeDispatch from './use-safe-dispatch'
  3. import { Nullable } from '../../../../types/utils'
  4. import { FetchError } from '../../infrastructure/fetch-json'
  5. type State<T, E> = {
  6. status: 'idle' | 'pending' | 'resolved' | 'rejected'
  7. data: Nullable<T>
  8. error: Nullable<E>
  9. }
  10. type Action<T, E> = Partial<State<T, E>>
  11. type AsyncRunner<T> = (signal: AbortSignal) => Promise<T>
  12. const defaultInitialState: State<null, null> = {
  13. status: 'idle',
  14. data: null,
  15. error: null,
  16. }
  17. const abortError = new Error('Aborted by the caller')
  18. abortError.name = 'AbortError'
  19. function useAsync<T = any, E extends Error | FetchError = Error>(
  20. initialState?: Partial<State<T, E>>
  21. ) {
  22. const initialStateRef = React.useRef({
  23. ...defaultInitialState,
  24. ...initialState,
  25. })
  26. // Use a Set to track all active AbortController instances
  27. const abortControllerSetRef = React.useRef<Set<AbortController>>(new Set())
  28. const [{ status, data, error }, setState] = React.useReducer(
  29. (state: State<T, E>, action: Action<T, E>) => ({ ...state, ...action }),
  30. initialStateRef.current
  31. )
  32. const safeSetState = useSafeDispatch(setState)
  33. const setData = React.useCallback(
  34. (data: Nullable<T>) => safeSetState({ data, status: 'resolved' }),
  35. [safeSetState]
  36. )
  37. const setError = React.useCallback(
  38. (error: Nullable<E>) => safeSetState({ error, status: 'rejected' }),
  39. [safeSetState]
  40. )
  41. const reset = React.useCallback(
  42. () => safeSetState(initialStateRef.current),
  43. [safeSetState]
  44. )
  45. const cancelAll = React.useCallback(() => {
  46. // Abort all controllers in the set and clear it
  47. abortControllerSetRef.current.forEach(controller => controller.abort())
  48. abortControllerSetRef.current.clear()
  49. }, [])
  50. const runAsync = React.useCallback(
  51. (asyncRunner: AsyncRunner<T>) => {
  52. safeSetState({ status: 'pending' })
  53. const controller = new AbortController()
  54. abortControllerSetRef.current.add(controller)
  55. // The original promise is now created using the provided factory function,
  56. // which receives the signal for cancellation.
  57. const promise = asyncRunner(controller.signal)
  58. const abortPromise = new Promise<never>((_resolve, reject) => {
  59. controller.signal.addEventListener('abort', () => {
  60. reject(abortError)
  61. })
  62. })
  63. return Promise.race([promise, abortPromise])
  64. .then(
  65. data => {
  66. setData(data)
  67. return data
  68. },
  69. error => {
  70. if (error !== abortError) {
  71. setError(error)
  72. }
  73. return Promise.reject(error)
  74. }
  75. )
  76. .finally(() => {
  77. // Remove the controller from the set, whether it succeeded or failed
  78. abortControllerSetRef.current.delete(controller)
  79. })
  80. },
  81. [safeSetState, setData, setError]
  82. )
  83. // Abort all requests when the component unmounts to prevent memory leaks
  84. React.useEffect(() => {
  85. return () => {
  86. cancelAll()
  87. }
  88. }, [cancelAll])
  89. return {
  90. isIdle: status === 'idle',
  91. isLoading: status === 'pending',
  92. isError: status === 'rejected',
  93. isSuccess: status === 'resolved',
  94. setData,
  95. setError,
  96. error,
  97. status,
  98. data,
  99. runAsync,
  100. reset,
  101. cancelAll,
  102. }
  103. }
  104. export default useAsync
  105. export { useAsync, abortError }