Skip to content

Commit bc4582b

Browse files
committed
Add use-promise
1 parent 7e273b5 commit bc4582b

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

hooks/use-promise.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useEffect, useMemo, useReducer, useRef } from 'react'
2+
3+
export type PromiseResult<T, P extends boolean, E = unknown> = {
4+
state: 'waiting'
5+
result: P extends true ? T | undefined : undefined
6+
error: P extends true ? E | undefined : undefined
7+
} | {
8+
state: 'resolved'
9+
result: T
10+
error: P extends true ? E | undefined : undefined
11+
} | {
12+
state: 'rejected'
13+
result: P extends true ? T | undefined : undefined
14+
error: E
15+
}
16+
17+
/**
18+
* A hook that dynamically refetches data on dependency update
19+
* @note The first-order function runs on server-side and client-side and determines whether the async second-order function should run client-side
20+
* @param fn The async function to run
21+
* @param deps The dependencies
22+
* @param persist Persist result values and error values into states that wouldn't normally have them
23+
* @returns An object containing the state and settled values
24+
*/
25+
export function useFetch<T, P extends boolean, E = unknown> (
26+
fn: () => false | undefined | null | '' | ((signal?: AbortSignal) => Promise<T>),
27+
deps: React.DependencyList = [],
28+
persist?: P
29+
): PromiseResult<T, P, E> {
30+
const value = useRef<PromiseResult<T, P, E>>({
31+
state: 'waiting',
32+
result: undefined,
33+
error: undefined
34+
})
35+
36+
// Manage renders manually so everything can be a ref for instantaneous state changes
37+
const [, rerender] = useReducer(() => ({}), {})
38+
39+
// useMemo runs before any other hook
40+
const callback = useMemo(() => {
41+
const cb = fn()
42+
43+
value.current = {
44+
state: 'waiting',
45+
result: persist ? value.current.result : undefined,
46+
error: persist ? value.current.error : undefined
47+
} as PromiseResult<T, P, E>
48+
49+
rerender()
50+
51+
return cb
52+
}, deps)
53+
54+
useEffect(() => {
55+
if (!callback) return
56+
57+
const aborter = new AbortController()
58+
59+
callback(aborter.signal)
60+
.then((result) => {
61+
if (aborter.signal.aborted) return // Don't act upon result
62+
value.current = {
63+
state: 'resolved',
64+
result,
65+
error: persist ? value.current.error : undefined
66+
} as PromiseResult<T, P, E>
67+
rerender()
68+
})
69+
.catch((err) => {
70+
if (!aborter.signal.aborted) {
71+
value.current = {
72+
state: 'rejected',
73+
result: persist ? value.current.result : undefined,
74+
error: err
75+
} as PromiseResult<T, P, E>
76+
rerender()
77+
}
78+
})
79+
80+
return () => aborter.abort()
81+
}, [callback])
82+
83+
return value.current
84+
}

0 commit comments

Comments
 (0)