Skip to content

Commit a73285b

Browse files
committed
useFetchedDataWithRefresh: Add, to use for read-receipts data soon
1 parent b44f14e commit a73285b

File tree

1 file changed

+88
-0
lines changed

1 file changed

+88
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* @flow strict-local */
2+
import { useState, useCallback, useRef } from 'react';
3+
4+
import { useConditionalEffect, useDebugAssertConstant, useHasStayedTrueForMs } from '../reactUtils';
5+
import { tryFetch } from '../message/fetchActions';
6+
7+
type SuccessResult<TData> = {| +type: 'success', +data: TData |};
8+
type FailResult = {| +type: 'error', +error: mixed |};
9+
type Result<TData> = SuccessResult<TData> | FailResult;
10+
11+
/**
12+
* Fetch and refresh data outside the event system, to show in the UI.
13+
*
14+
* Some data, like read receipts, isn't provided via the event system and
15+
* must be fetched with an API call. Use this Hook to maintain an
16+
* automatically refreshed shapshot of the data in a React component.
17+
*
18+
* The returned `latestResult` and `latestSuccessResult` will be unique per
19+
* request and will be === when the latest result was successful.
20+
*
21+
* Of course, any API call can fail or take longer than expected. Callers
22+
* should use this Hook's output to inform the user when this is the case.
23+
* For example:
24+
*
25+
* const { latestResult, latestSuccessResult } = useFetchedDataWithRefresh(…);
26+
* const latestResultIsError = latestResult?.type === 'error';
27+
* const isFirstLoadLate = useHasStayedTrueForMs(latestSuccessResult === null, 10_000);
28+
* const haveStaleData =
29+
* useHasNotChangedForMs(latestSuccessResult, 40_000) && latestSuccessResult !== null;
30+
*
31+
* Still, this Hook handles its own retry logic and will do its best to
32+
* fetch and show the data for as long as the calling React component is
33+
* mounted.
34+
*
35+
* @param {callApiMethod} - E.g., a function that returns the Promise from
36+
* api.getReadReceipts(…), to fetch read receipts.
37+
* @param {refreshIntervalMs} - How long to wait after the latest response
38+
* (success or failure) before requesting again.
39+
*/
40+
export default function useFetchedDataWithRefresh<TData>(
41+
callApiMethod: () => Promise<TData>,
42+
refreshIntervalMs: number,
43+
): {|
44+
+latestResult: null | Result<TData>,
45+
+latestSuccessResult: null | SuccessResult<TData>,
46+
|} {
47+
useDebugAssertConstant(refreshIntervalMs);
48+
49+
const [isFetching, setIsFetching] = useState(false);
50+
const [latestResult, setLatestResult] = useState(null);
51+
const [latestSuccessResult, setLatestSuccessResult] = useState(null);
52+
53+
const fetch = useCallback(async () => {
54+
setIsFetching(true);
55+
try {
56+
const data: TData = await tryFetch(callApiMethod);
57+
const result = { type: 'success', data };
58+
setLatestResult(result);
59+
setLatestSuccessResult(result);
60+
} catch (errorIllTyped) {
61+
const error: mixed = errorIllTyped; // https://github.com/facebook/flow/issues/2470
62+
const result = { type: 'error', error };
63+
setLatestResult(result);
64+
} finally {
65+
setIsFetching(false);
66+
}
67+
}, [callApiMethod]);
68+
69+
const startFetchIfNotFetching = useCallback(() => {
70+
if (isFetching) {
71+
return;
72+
}
73+
74+
fetch();
75+
}, [isFetching, fetch]);
76+
77+
const isFirstCall = useRef(true);
78+
const shouldRefresh =
79+
useHasStayedTrueForMs(!isFetching, refreshIntervalMs) || isFirstCall.current;
80+
isFirstCall.current = false;
81+
82+
useConditionalEffect(startFetchIfNotFetching, shouldRefresh);
83+
84+
return {
85+
latestResult,
86+
latestSuccessResult,
87+
};
88+
}

0 commit comments

Comments
 (0)