|
1 |
| -import { useCallback, useEffect, useRef, useState } from 'react'; |
| 1 | +import { |
| 2 | + useCallback, |
| 3 | + useEffect, |
| 4 | + useLayoutEffect, |
| 5 | + useRef, |
| 6 | + useState, |
| 7 | +} from 'react'; |
| 8 | + |
| 9 | +// See https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 |
| 10 | +const useIsomorphicLayoutEffect = |
| 11 | + typeof window !== 'undefined' && |
| 12 | + typeof window.document !== 'undefined' && |
| 13 | + typeof window.document.createElement !== 'undefined' |
| 14 | + ? useLayoutEffect |
| 15 | + : useEffect; |
| 16 | + |
| 17 | +// assign current value to a ref and providing a getter. |
| 18 | +// This way we are sure to always get latest value provided to hook and |
| 19 | +// avoid weird issues due to closures capturing stale values... |
| 20 | +// See https://overreacted.io/making-setinterval-declarative-with-react-hooks/ |
| 21 | +const useGetter = <T>(t: T) => { |
| 22 | + const ref = useRef(t); |
| 23 | + useIsomorphicLayoutEffect(() => { |
| 24 | + ref.current = t; |
| 25 | + }); |
| 26 | + return () => ref.current; |
| 27 | +}; |
2 | 28 |
|
3 | 29 | type UnknownResult = unknown;
|
4 | 30 |
|
@@ -106,6 +132,7 @@ const normalizeOptions = <R>(
|
106 | 132 | type UseAsyncStateResult<R> = {
|
107 | 133 | value: AsyncState<R>;
|
108 | 134 | set: (value: AsyncState<R>) => void;
|
| 135 | + merge: (value: Partial<AsyncState<R>>) => void; |
109 | 136 | reset: () => void;
|
110 | 137 | setLoading: () => void;
|
111 | 138 | setResult: (r: R) => void;
|
@@ -137,9 +164,20 @@ const useAsyncState = <R extends {}>(
|
137 | 164 | [value, setValue]
|
138 | 165 | );
|
139 | 166 |
|
| 167 | + const set = setValue; |
| 168 | + |
| 169 | + const merge = useCallback( |
| 170 | + (state: Partial<AsyncState<R>>) => |
| 171 | + set({ |
| 172 | + ...value, |
| 173 | + ...state, |
| 174 | + }), |
| 175 | + [value, set] |
| 176 | + ); |
140 | 177 | return {
|
141 | 178 | value,
|
142 |
| - set: setValue, |
| 179 | + set, |
| 180 | + merge, |
143 | 181 | reset,
|
144 | 182 | setLoading,
|
145 | 183 | setResult,
|
@@ -177,6 +215,7 @@ export type UseAsyncReturn<
|
177 | 215 | Args extends any[] = UnknownArgs
|
178 | 216 | > = AsyncState<R> & {
|
179 | 217 | set: (value: AsyncState<R>) => void;
|
| 218 | + merge: (value: Partial<AsyncState<R>>) => void; |
180 | 219 | reset: () => void;
|
181 | 220 | execute: (...args: Args) => Promise<R>;
|
182 | 221 | currentPromise: Promise<R> | null;
|
@@ -251,6 +290,7 @@ const useAsyncInternal = <R = UnknownResult, Args extends any[] = UnknownArgs>(
|
251 | 290 | return {
|
252 | 291 | ...AsyncState.value,
|
253 | 292 | set: AsyncState.set,
|
| 293 | + merge: AsyncState.merge, |
254 | 294 | reset: AsyncState.reset,
|
255 | 295 | execute: executeAsyncOperation,
|
256 | 296 | currentPromise: CurrentPromise.get(),
|
@@ -353,3 +393,84 @@ export const useAsyncCallback = <
|
353 | 393 | }
|
354 | 394 | );
|
355 | 395 | };
|
| 396 | + |
| 397 | +export const useAsyncFetchMore = <R, Args extends any[]>({ |
| 398 | + value, |
| 399 | + fetchMore, |
| 400 | + merge, |
| 401 | + isEnd: isEndFn, |
| 402 | +}: { |
| 403 | + value: UseAsyncReturn<R, Args>; |
| 404 | + fetchMore: (result: R) => Promise<R>; |
| 405 | + merge: (result: R, moreResult: R) => R; |
| 406 | + isEnd: (moreResult: R) => boolean; |
| 407 | +}) => { |
| 408 | + const getAsyncValue = useGetter(value); |
| 409 | + const [isEnd, setIsEnd] = useState(false); |
| 410 | + |
| 411 | + // TODO not really fan of this id thing, we should find a way to support cancellation! |
| 412 | + const fetchMoreId = useRef(0); |
| 413 | + |
| 414 | + const fetchMoreAsync = useAsyncCallback(async () => { |
| 415 | + const freshAsyncValue = getAsyncValue(); |
| 416 | + if (freshAsyncValue.status !== 'success') { |
| 417 | + throw new Error( |
| 418 | + "Can't fetch more if the original fetch is not a success" |
| 419 | + ); |
| 420 | + } |
| 421 | + if (fetchMoreAsync.status === 'loading') { |
| 422 | + throw new Error( |
| 423 | + "Can't fetch more, because we are already fetching more!" |
| 424 | + ); |
| 425 | + } |
| 426 | + |
| 427 | + fetchMoreId.current = fetchMoreId.current + 1; |
| 428 | + const currentId = fetchMoreId.current; |
| 429 | + const moreResult = await fetchMore(freshAsyncValue.result!); |
| 430 | + |
| 431 | + // TODO not satisfied with this, we should just use "freshAsyncValue === getAsyncValue()" but asyncValue is not "stable" |
| 432 | + const isStillSameValue = |
| 433 | + freshAsyncValue.status === getAsyncValue().status && |
| 434 | + freshAsyncValue.result === getAsyncValue().result; |
| 435 | + |
| 436 | + const isStillSameId = fetchMoreId.current === currentId; |
| 437 | + |
| 438 | + // Handle race conditions: we only merge the fetchMore result if the initial async value is the same |
| 439 | + const canMerge = isStillSameValue && isStillSameId; |
| 440 | + if (canMerge) { |
| 441 | + value.merge({ |
| 442 | + result: merge(value.result!, moreResult), |
| 443 | + }); |
| 444 | + if (isEndFn(moreResult)) { |
| 445 | + setIsEnd(true); |
| 446 | + } |
| 447 | + } |
| 448 | + |
| 449 | + // return is useful for chaining, like fetchMore().then(result => {}); |
| 450 | + return moreResult; |
| 451 | + }); |
| 452 | + |
| 453 | + const reset = () => { |
| 454 | + fetchMoreAsync.reset(); |
| 455 | + setIsEnd(false); |
| 456 | + }; |
| 457 | + |
| 458 | + // We only allow to fetch more on a stable async value |
| 459 | + // If that value change for whatever reason, we reset the fetchmore too (which will make current pending requests to be ignored) |
| 460 | + // TODO value is not stable, we could just reset on value change otherwise |
| 461 | + const shouldReset = value.status !== 'success'; |
| 462 | + useEffect(() => { |
| 463 | + if (shouldReset) { |
| 464 | + reset(); |
| 465 | + } |
| 466 | + }, [shouldReset]); |
| 467 | + |
| 468 | + return { |
| 469 | + canFetchMore: |
| 470 | + value.status === 'success' && fetchMoreAsync.status !== 'loading', |
| 471 | + loading: fetchMoreAsync.loading, |
| 472 | + status: fetchMoreAsync.status, |
| 473 | + fetchMore: fetchMoreAsync.execute, |
| 474 | + isEnd, |
| 475 | + }; |
| 476 | +}; |
0 commit comments