From 97646f98de20a366122f4ea86c82e29280c4ff72 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 12 Nov 2023 22:43:36 +0000 Subject: [PATCH] wip: missing file --- webui/src/Util/usePoller.ts | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 webui/src/Util/usePoller.ts diff --git a/webui/src/Util/usePoller.ts b/webui/src/Util/usePoller.ts new file mode 100644 index 0000000..37bbce6 --- /dev/null +++ b/webui/src/Util/usePoller.ts @@ -0,0 +1,75 @@ +import { useEffect, useReducer, useRef, useState } from 'react' +import { useInterval } from 'usehooks-ts' + +interface State { + data?: T + error?: Error + lastPoll?: number +} + +// discriminated union type +type Action = { type: 'loading' } | { type: 'fetched'; payload: T } | { type: 'error'; payload: Error } + +export function usePoller(interval: number, doPoll: (() => Promise) | undefined): State { + // Invalidate the request on an interval + const [refreshToken, setRefreshToken] = useState(Date.now()) + useInterval(() => setRefreshToken(Date.now()), interval) + + // Used to prevent state update if the component is unmounted + const cancelRequest = useRef(false) + + const initialState: State = { + error: undefined, + data: undefined, + lastPoll: undefined, + } + + // Keep state logic separated + const fetchReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'loading': + return { ...initialState } + case 'fetched': + return { ...initialState, data: action.payload, lastPoll: Date.now() } + case 'error': + return { ...initialState, error: action.payload, lastPoll: Date.now() } + default: + return state + } + } + + const [state, dispatch] = useReducer(fetchReducer, initialState) + + useEffect(() => { + // Do nothing if the url is not given + if (!doPoll) return + + cancelRequest.current = false + + const fetchData = async () => { + // dispatch({ type: 'loading' }) + + try { + const data = await doPoll() + if (cancelRequest.current) return + + dispatch({ type: 'fetched', payload: data }) + } catch (error) { + if (cancelRequest.current) return + + dispatch({ type: 'error', payload: error as Error }) + } + } + + void fetchData() + + // Use the cleanup function for avoiding a possibly... + // ...state update after the component was unmounted + return () => { + cancelRequest.current = true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [doPoll, refreshToken]) + + return state +}