|
1 | 1 | import {
|
2 |
| - useEffect, |
| 2 | + useLayoutEffect, |
3 | 3 | useReducer,
|
| 4 | + useRef, |
4 | 5 | } from 'react';
|
5 |
| -import { useMemoOne as useMemo } from 'use-memo-one'; |
6 | 6 |
|
7 |
| -const createTask = (func, forceUpdate) => { |
8 |
| - const task = { |
9 |
| - abortController: null, |
| 7 | +const createTask = ({ func, dispatchRef }) => { |
| 8 | + const taskId = Symbol('TASK_ID'); |
| 9 | + let abortController = null; |
| 10 | + return { |
| 11 | + func, |
| 12 | + taskId, |
| 13 | + runId: null, |
10 | 14 | start: async (...args) => {
|
11 |
| - if (task.id === null) { |
12 |
| - // already cleaned up |
13 |
| - return null; |
| 15 | + if (abortController) { |
| 16 | + abortController.abort(); |
14 | 17 | }
|
15 |
| - task.abort(); |
16 |
| - task.abortController = new AbortController(); |
17 |
| - const taskId = Symbol('TASK_ID'); |
18 |
| - task.id = taskId; |
19 |
| - task.started = true; |
20 |
| - task.pending = true; |
21 |
| - task.error = null; |
22 |
| - task.result = null; |
23 |
| - forceUpdate(); |
| 18 | + abortController = new AbortController(); |
| 19 | + const runId = Symbol('RUN_ID'); |
| 20 | + dispatchRef.current({ |
| 21 | + type: 'START', |
| 22 | + taskId, |
| 23 | + runId, |
| 24 | + }); |
24 | 25 | let result = null;
|
25 |
| - let err = null; |
| 26 | + let error = null; |
26 | 27 | try {
|
27 |
| - result = await func(task.abortController, ...args); |
| 28 | + result = await func(abortController, ...args); |
28 | 29 | } catch (e) {
|
29 | 30 | if (e.name !== 'AbortError') {
|
30 |
| - err = e; |
| 31 | + error = e; |
31 | 32 | }
|
32 | 33 | }
|
33 |
| - if (task.id === taskId) { |
34 |
| - task.result = result; |
35 |
| - task.error = err; |
36 |
| - task.started = false; |
37 |
| - task.pending = false; |
38 |
| - forceUpdate(); |
39 |
| - } |
40 |
| - if (err) throw err; |
| 34 | + dispatchRef.current({ |
| 35 | + type: 'END', |
| 36 | + taskId, |
| 37 | + runId, |
| 38 | + result, |
| 39 | + error, |
| 40 | + }); |
| 41 | + if (error) throw error; |
41 | 42 | return result;
|
42 | 43 | },
|
43 | 44 | abort: () => {
|
44 |
| - if (task.abortController) { |
45 |
| - task.abortController.abort(); |
46 |
| - task.abortController = null; |
| 45 | + if (abortController) { |
| 46 | + abortController.abort(); |
| 47 | + abortController = null; |
47 | 48 | }
|
48 | 49 | },
|
49 |
| - id: 0, |
50 | 50 | started: false,
|
51 | 51 | pending: true,
|
52 | 52 | error: null,
|
53 | 53 | result: null,
|
54 | 54 | };
|
55 |
| - return task; |
| 55 | +}; |
| 56 | + |
| 57 | +const reducer = (task, action) => { |
| 58 | + switch (action.type) { |
| 59 | + case 'INIT': |
| 60 | + return createTask(action); |
| 61 | + case 'START': |
| 62 | + if (task.taskId !== action.taskId) { |
| 63 | + return task; // bail out |
| 64 | + } |
| 65 | + return { |
| 66 | + ...task, |
| 67 | + runId: action.runId, |
| 68 | + started: true, |
| 69 | + pending: true, |
| 70 | + error: null, |
| 71 | + result: null, |
| 72 | + }; |
| 73 | + case 'END': |
| 74 | + if (task.taskId !== action.taskId || task.runId !== action.runId) { |
| 75 | + return task; // bail out |
| 76 | + } |
| 77 | + return { |
| 78 | + ...task, |
| 79 | + started: false, |
| 80 | + pending: false, |
| 81 | + error: action.error, |
| 82 | + result: action.result, |
| 83 | + }; |
| 84 | + default: |
| 85 | + throw new Error(`unknown action type: ${action.type}`); |
| 86 | + } |
56 | 87 | };
|
57 | 88 |
|
58 | 89 | export const useAsyncTask = (func) => {
|
59 |
| - const [, forceUpdate] = useReducer(c => c + 1, 0); |
60 |
| - const task = useMemo(() => createTask(func, forceUpdate), [func]); |
61 |
| - useEffect(() => { |
| 90 | + const dispatchRef = useRef(() => { throw new Error('not initialized'); }); |
| 91 | + const [task, dispatch] = useReducer(reducer, { func, dispatchRef }, createTask); |
| 92 | + useLayoutEffect(() => { |
| 93 | + if (task.func !== func) { |
| 94 | + dispatch({ type: 'INIT', func, dispatchRef }); |
| 95 | + } |
| 96 | + }); |
| 97 | + useLayoutEffect(() => { |
| 98 | + dispatchRef.current = dispatch; |
62 | 99 | const cleanup = () => {
|
63 |
| - task.id = null; |
64 |
| - task.abort(); |
| 100 | + dispatchRef.current = () => {}; |
65 | 101 | };
|
66 | 102 | return cleanup;
|
67 |
| - }, [task]); |
68 |
| - return useMemo(() => ({ |
69 |
| - start: task.start, |
70 |
| - abort: task.abort, |
71 |
| - started: task.started, |
72 |
| - pending: task.pending, |
73 |
| - error: task.error, |
74 |
| - result: task.result, |
75 |
| - }), [ |
76 |
| - task.start, |
77 |
| - task.abort, |
78 |
| - task.started, |
79 |
| - task.pending, |
80 |
| - task.error, |
81 |
| - task.result, |
82 |
| - ]); |
| 103 | + }, []); |
| 104 | + return task; |
83 | 105 | };
|
0 commit comments