UNPKG

react-hooks-async

Version:

React custom hooks for async functions with abortability and composability

112 lines (107 loc) 2.86 kB
import { useEffect, useReducer, useRef } from 'react'; export const SYMBOL_ABORTED = Symbol('ABORTED'); const createTask = ({ func, dispatchRef }) => { const taskId = Symbol('TASK_ID'); let abortController = null; return { func, taskId, runId: null, start: async (...args) => { if (abortController) { abortController.abort(); } abortController = new AbortController(); const runId = Symbol('RUN_ID'); dispatchRef.current({ type: 'START', taskId, runId }); try { const result = await func(abortController, ...args); dispatchRef.current({ type: 'RESULT', taskId, runId, result }); return result; } catch (error) { if (error.name === 'AbortError') { dispatchRef.current({ type: 'ABORT', taskId, runId }); return SYMBOL_ABORTED; } dispatchRef.current({ type: 'ERROR', taskId, runId, error }); throw error; } }, abort: () => { if (abortController) { abortController.abort(); abortController = null; } }, started: false, pending: true, aborted: false, error: null, result: null, }; }; const reducer = (task, action) => { switch (action.type) { case 'INIT': return createTask(action); case 'START': if (task.taskId !== action.taskId) { return task; // bail out } return { ...task, runId: action.runId, started: true, pending: true, aborted: false, error: null, result: null, }; case 'RESULT': if (task.taskId !== action.taskId || task.runId !== action.runId) { return task; // bail out } return { ...task, started: false, pending: false, result: action.result, }; case 'ABORT': if (task.taskId !== action.taskId || task.runId !== action.runId) { return task; // bail out } return { ...task, started: false, aborted: true, }; case 'ERROR': if (task.taskId !== action.taskId || task.runId !== action.runId) { return task; // bail out } return { ...task, started: false, pending: false, error: action.error, }; default: throw new Error(`unknown action type: ${action.type}`); } }; export const useAsyncTask = (func) => { const dispatchRef = useRef(() => { throw new Error('not initialized'); }); const [task, dispatch] = useReducer(reducer, { func, dispatchRef }, createTask); if (task.func !== func) { dispatch({ type: 'INIT', func, dispatchRef }); } dispatchRef.current = dispatch; useEffect(() => { const cleanup = () => { dispatchRef.current = () => null; }; return cleanup; }, []); return task; };