|
| 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