1 | import { useCallback, useEffect, useRef, useState } from 'react';
|
2 | /**
|
3 | * A hook that mirrors `useState` in function and API, expect that setState
|
4 | * calls return a promise that resolves after the state has been set (in an effect).
|
5 | *
|
6 | * This is _similar_ to the second callback in classy setState calls, but fires later.
|
7 | *
|
8 | * ```ts
|
9 | * const [counter, setState] = useStateAsync(1);
|
10 | *
|
11 | * const handleIncrement = async () => {
|
12 | * await setState(2);
|
13 | * doWorkRequiringCurrentState()
|
14 | * }
|
15 | * ```
|
16 | *
|
17 | * @param initialState initialize with some state value same as `useState`
|
18 | */
|
19 | export default function useStateAsync(initialState) {
|
20 | const [state, setState] = useState(initialState);
|
21 | const resolvers = useRef([]);
|
22 | useEffect(() => {
|
23 | resolvers.current.forEach(resolve => resolve(state));
|
24 | resolvers.current.length = 0;
|
25 | }, [state]);
|
26 | const setStateAsync = useCallback(update => {
|
27 | return new Promise((resolve, reject) => {
|
28 | setState(prevState => {
|
29 | try {
|
30 | let nextState;
|
31 | // ugly instanceof for typescript
|
32 | if (update instanceof Function) {
|
33 | nextState = update(prevState);
|
34 | } else {
|
35 | nextState = update;
|
36 | }
|
37 |
|
38 | // If state does not change, we must resolve the promise because
|
39 | // react won't re-render and effect will not resolve. If there are already
|
40 | // resolvers queued, then it should be safe to assume an update will happen
|
41 | if (!resolvers.current.length && Object.is(nextState, prevState)) {
|
42 | resolve(nextState);
|
43 | } else {
|
44 | resolvers.current.push(resolve);
|
45 | }
|
46 | return nextState;
|
47 | } catch (e) {
|
48 | reject(e);
|
49 | throw e;
|
50 | }
|
51 | });
|
52 | });
|
53 | }, [setState]);
|
54 | return [state, setStateAsync];
|
55 | } |
\ | No newline at end of file |