Skip to content

Commit d5681a9

Browse files
committed
Pass options into promiseFn instead of lastOptions
1 parent 059b1d6 commit d5681a9

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

packages/react-async/src/useAsync.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { useCallback, useDebugValue, useEffect, useMemo, useRef, useReducer } from "react"
2+
3+
import globalScope from "./globalScope"
4+
import {
5+
neverSettle,
6+
actionTypes,
7+
init,
8+
dispatchMiddleware,
9+
reducer as asyncReducer,
10+
} from "./reducer"
11+
12+
const noop = () => {}
13+
14+
const useAsync = (arg1, arg2) => {
15+
const options = typeof arg1 === "function" ? { ...arg2, promiseFn: arg1 } : arg1
16+
17+
const counter = useRef(0)
18+
const isMounted = useRef(true)
19+
const lastArgs = useRef(undefined)
20+
const lastOptions = useRef(undefined)
21+
const lastPromise = useRef(neverSettle)
22+
const abortController = useRef({ abort: noop })
23+
24+
const { devToolsDispatcher } = globalScope.__REACT_ASYNC__
25+
const { reducer, dispatcher = devToolsDispatcher } = options
26+
const [state, _dispatch] = useReducer(
27+
reducer ? (state, action) => reducer(state, action, asyncReducer) : asyncReducer,
28+
options,
29+
init
30+
)
31+
const dispatch = useCallback(
32+
dispatcher
33+
? action => dispatcher(action, dispatchMiddleware(_dispatch), lastOptions.current)
34+
: dispatchMiddleware(_dispatch),
35+
[dispatcher]
36+
)
37+
38+
const { debugLabel } = options
39+
const getMeta = useCallback(
40+
meta => ({ counter: counter.current, promise: lastPromise.current, debugLabel, ...meta }),
41+
[debugLabel]
42+
)
43+
44+
const setData = useCallback(
45+
(data, callback = noop) => {
46+
if (isMounted.current) {
47+
dispatch({ type: actionTypes.fulfill, payload: data, meta: getMeta() })
48+
callback()
49+
}
50+
return data
51+
},
52+
[dispatch, getMeta]
53+
)
54+
55+
const setError = useCallback(
56+
(error, callback = noop) => {
57+
if (isMounted.current) {
58+
dispatch({ type: actionTypes.reject, payload: error, error: true, meta: getMeta() })
59+
callback()
60+
}
61+
return error
62+
},
63+
[dispatch, getMeta]
64+
)
65+
66+
const { onResolve, onReject } = options
67+
const handleResolve = useCallback(
68+
count => data => count === counter.current && setData(data, () => onResolve && onResolve(data)),
69+
[setData, onResolve]
70+
)
71+
const handleReject = useCallback(
72+
count => err => count === counter.current && setError(err, () => onReject && onReject(err)),
73+
[setError, onReject]
74+
)
75+
76+
const start = useCallback(
77+
promiseFn => {
78+
if ("AbortController" in globalScope) {
79+
abortController.current.abort()
80+
abortController.current = new globalScope.AbortController()
81+
}
82+
counter.current++
83+
return (lastPromise.current = new Promise((resolve, reject) => {
84+
if (!isMounted.current) return
85+
const executor = () => promiseFn().then(resolve, reject)
86+
dispatch({ type: actionTypes.start, payload: executor, meta: getMeta() })
87+
}))
88+
},
89+
[dispatch, getMeta]
90+
)
91+
92+
const { promise, promiseFn, initialValue } = options
93+
const load = useCallback(() => {
94+
const isPreInitialized = initialValue && counter.current === 0
95+
if (promise) {
96+
start(() => promise)
97+
.then(handleResolve(counter.current))
98+
.catch(handleReject(counter.current))
99+
} else if (promiseFn && !isPreInitialized) {
100+
start(() => promiseFn(options, abortController.current))
101+
.then(handleResolve(counter.current))
102+
.catch(handleReject(counter.current))
103+
}
104+
}, [initialValue, promise, promiseFn, start, handleResolve, handleReject, options])
105+
106+
const { deferFn } = options
107+
const run = useCallback(
108+
(...args) => {
109+
if (deferFn) {
110+
lastArgs.current = args
111+
start(() => deferFn(args, lastOptions.current, abortController.current))
112+
.then(handleResolve(counter.current))
113+
.catch(handleReject(counter.current))
114+
}
115+
},
116+
[start, deferFn, handleResolve, handleReject]
117+
)
118+
119+
const reload = useCallback(() => {
120+
lastArgs.current ? run(...lastArgs.current) : load()
121+
}, [run, load])
122+
123+
const { onCancel } = options
124+
const cancel = useCallback(() => {
125+
onCancel && onCancel()
126+
counter.current++
127+
abortController.current.abort()
128+
isMounted.current && dispatch({ type: actionTypes.cancel, meta: getMeta() })
129+
}, [onCancel, dispatch, getMeta])
130+
131+
/* These effects should only be triggered on changes to specific props */
132+
/* eslint-disable react-hooks/exhaustive-deps */
133+
const { watch, watchFn } = options
134+
useEffect(() => {
135+
if (watchFn && lastOptions.current && watchFn(options, lastOptions.current)) load()
136+
})
137+
useEffect(() => {
138+
lastOptions.current = options
139+
}, [options])
140+
useEffect(() => {
141+
if (counter.current) cancel()
142+
if (promise || promiseFn) load()
143+
}, [promise, promiseFn, watch])
144+
useEffect(() => () => (isMounted.current = false), [])
145+
useEffect(() => () => cancel(), [])
146+
/* eslint-enable react-hooks/exhaustive-deps */
147+
148+
useDebugValue(state, ({ status }) => `[${counter.current}] ${status}`)
149+
150+
if (options.suspense && state.isPending && lastPromise.current !== neverSettle) {
151+
// Rely on Suspense to handle the loading state
152+
throw lastPromise.current
153+
}
154+
155+
return useMemo(
156+
() => ({
157+
...state,
158+
run,
159+
reload,
160+
cancel,
161+
setData,
162+
setError,
163+
}),
164+
[state, run, reload, cancel, setData, setError]
165+
)
166+
}
167+
168+
export class FetchError extends Error {
169+
constructor(response) {
170+
super(`${response.status} ${response.statusText}`)
171+
/* istanbul ignore next */
172+
if (Object.setPrototypeOf) {
173+
// Not available in IE 10, but can be polyfilled
174+
Object.setPrototypeOf(this, FetchError.prototype)
175+
}
176+
this.response = response
177+
}
178+
}
179+
180+
const parseResponse = (accept, json) => res => {
181+
if (!res.ok) return Promise.reject(new FetchError(res))
182+
if (typeof json === "boolean") return json ? res.json() : res
183+
return accept === "application/json" ? res.json() : res
184+
}
185+
186+
const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => {
187+
const method = resource.method || (init && init.method)
188+
const headers = resource.headers || (init && init.headers) || {}
189+
const accept = headers["Accept"] || headers["accept"] || (headers.get && headers.get("accept"))
190+
const doFetch = (resource, init) =>
191+
globalScope.fetch(resource, init).then(parseResponse(accept, json))
192+
const isDefer =
193+
typeof defer === "boolean" ? defer : ["POST", "PUT", "PATCH", "DELETE"].indexOf(method) !== -1
194+
const fn = isDefer ? "deferFn" : "promiseFn"
195+
const identity = JSON.stringify({ resource, init, isDefer })
196+
const state = useAsync({
197+
...options,
198+
[fn]: useCallback(
199+
(arg1, arg2, arg3) => {
200+
const [override, signal] = isDefer ? [arg1[0], arg3.signal] : [undefined, arg2.signal]
201+
const isEvent = typeof override === "object" && "preventDefault" in override
202+
if (!override || isEvent) {
203+
return doFetch(resource, { signal, ...init })
204+
}
205+
if (typeof override === "function") {
206+
const { resource: runResource, ...runInit } = override({ resource, signal, ...init })
207+
return doFetch(runResource || resource, { signal, ...runInit })
208+
}
209+
const { resource: runResource, ...runInit } = override
210+
return doFetch(runResource || resource, { signal, ...init, ...runInit })
211+
},
212+
[identity] // eslint-disable-line react-hooks/exhaustive-deps
213+
),
214+
})
215+
useDebugValue(state, ({ counter, status }) => `[${counter}] ${status}`)
216+
return state
217+
}
218+
219+
/* istanbul ignore next */
220+
const unsupported = () => {
221+
throw new Error(
222+
"useAsync requires React v16.8 or up. Upgrade your React version or use the <Async> component instead."
223+
)
224+
}
225+
226+
export default useEffect ? useAsync : unsupported
227+
export const useFetch = useEffect ? useAsyncFetch : unsupported

0 commit comments

Comments
 (0)