diff --git a/packages/react-hooks/src/ctx/ApiStats.tsx b/packages/react-hooks/src/ctx/ApiStats.tsx index 2438b9237c..c0da2075f1 100644 --- a/packages/react-hooks/src/ctx/ApiStats.tsx +++ b/packages/react-hooks/src/ctx/ApiStats.tsx @@ -5,10 +5,10 @@ import type { ApiPromise } from '@polkadot/api'; import type { ProviderStats } from '@polkadot/rpc-provider/types'; import type { ApiStats } from './types'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback } from 'react'; import { useApi } from '../useApi'; -import { useIsMountedRef } from '../useIsMountedRef'; +import { useTimer } from '../useTimer'; interface Props { children?: React.ReactNode; @@ -35,10 +35,6 @@ function getStats (...apis: ApiPromise[]): { stats: ProviderStats, when: number } }; - if (apis.length === 0) { - return { stats, when: Date.now() }; - } - for (let i = 0; i < apis.length; i++) { const s = apis[i].stats; @@ -61,42 +57,26 @@ function getStats (...apis: ApiPromise[]): { stats: ProviderStats, when: number }; } +function mergeStats (curr: ApiStats, prev: ApiStats[]): ApiStats[] { + return prev.length === 0 + ? [curr] + : prev.length === MAX_NUM + ? prev.concat(curr).slice(-MAX_NUM) + : prev.concat(curr); +} + export const ApiStatsCtx = React.createContext(EMPTY_STATE); export function ApiStatsCtxRoot ({ children }: Props): React.ReactElement { const { api } = useApi(); - const [stats, setStats] = useState(EMPTY_STATE); - const timerId = useRef | null>(null); - const mountedRef = useIsMountedRef(); - useEffect((): () => void => { - function fireTimer (): void { - timerId.current = null; + const stateFn = useCallback( + (prev: ApiStats[]): ApiStats[] => + mergeStats(getStats(api), prev), + [api] + ); - if (mountedRef.current) { - const curr = getStats(api); - - setStats((prev): ApiStats[] => { - if (prev.length === 0) { - return [curr]; - } - - return prev.length === MAX_NUM - ? prev.concat(curr).slice(-MAX_NUM) - : prev.concat(curr); - }); - - timerId.current = setTimeout(fireTimer, INTERVAL); - } - } - - fireTimer(); - - return (): void => { - timerId.current && clearTimeout(timerId.current); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const stats = useTimer(stateFn, EMPTY_STATE, INTERVAL); return ( diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index bd471f1890..941be94622 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -73,6 +73,7 @@ export { useSudo } from './useSudo'; export { useSystemApi } from './useSystemApi'; export { useTeleport } from './useTeleport'; export { useTheme } from './useTheme'; +export { useTimer } from './useTimer'; export { useToggle } from './useToggle'; export { useTreasury } from './useTreasury'; export { useTxBatch } from './useTxBatch'; diff --git a/packages/react-hooks/src/useDebounce.ts b/packages/react-hooks/src/useDebounce.ts index 308e0f554d..503d7b1ae6 100644 --- a/packages/react-hooks/src/useDebounce.ts +++ b/packages/react-hooks/src/useDebounce.ts @@ -8,18 +8,18 @@ import { useIsMountedRef } from './useIsMountedRef'; const DEFAULT_DELAY = 250; // FIXE Due to generics, cannot use createNamedHook -export function useDebounce (value: T, delay?: number): T { +export function useDebounce (value: T, delay = DEFAULT_DELAY): T { const mountedRef = useIsMountedRef(); const [debouncedValue, setDebouncedValue] = useState(value); useEffect((): () => void => { - const handler = setTimeout(() => { + const timeoutId = setTimeout(() => { mountedRef.current && setDebouncedValue(value); - }, delay || DEFAULT_DELAY); + }, delay); - // each time it renders, it clears + // each time something changes, we clears return (): void => { - clearTimeout(handler); + clearTimeout(timeoutId); }; }, [delay, value, mountedRef]); diff --git a/packages/react-hooks/src/useTimer.ts b/packages/react-hooks/src/useTimer.ts new file mode 100644 index 0000000000..f452422980 --- /dev/null +++ b/packages/react-hooks/src/useTimer.ts @@ -0,0 +1,37 @@ +// Copyright 2017-2023 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useRef, useState } from 'react'; + +import { useIsMountedRef } from './useIsMountedRef'; + +export function useTimer (stateFn: (prev: T) => T, initial: T, interval: number): T { + const [state, setState] = useState(initial); + const timerId = useRef | null>(null); + const mountedRef = useIsMountedRef(); + + useEffect((): () => void => { + function fire (): void { + timerId.current = null; + + if (mountedRef.current) { + try { + setState(stateFn); + } catch (e) { + console.error(e); + } + + timerId.current = setTimeout(fire, interval); + } + } + + fire(); + + return (): void => { + timerId.current && clearTimeout(timerId.current); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return state; +}