From ba86c6a1114fd4d0f54cff174c117103f91bac47 Mon Sep 17 00:00:00 2001 From: Jaco Date: Tue, 10 Jan 2023 14:09:16 +0200 Subject: [PATCH] Pull in useMemoValue (& basic use) (#8750) --- packages/page-explorer/src/Latency/index.tsx | 93 ++++++++++++------- .../page-explorer/src/Latency/useLatency.ts | 2 +- packages/react-hooks/src/index.ts | 1 + packages/react-hooks/src/useEventChanges.ts | 4 +- packages/react-hooks/src/useEventTrigger.ts | 9 +- .../react-hooks/src/useExtrinsicTrigger.ts | 6 +- packages/react-hooks/src/useMemoValue.spec.ts | 60 ++++++++++++ packages/react-hooks/src/useMemoValue.ts | 70 ++++++++++++++ 8 files changed, 202 insertions(+), 43 deletions(-) create mode 100644 packages/react-hooks/src/useMemoValue.spec.ts create mode 100644 packages/react-hooks/src/useMemoValue.ts diff --git a/packages/page-explorer/src/Latency/index.tsx b/packages/page-explorer/src/Latency/index.tsx index 06ddc21861..d7324df357 100644 --- a/packages/page-explorer/src/Latency/index.tsx +++ b/packages/page-explorer/src/Latency/index.tsx @@ -91,7 +91,7 @@ function formatTime (time: number, divisor = 1000): string { return `${(time / divisor).toFixed(3)}s`; } -function Latency ({ className }: Props): React.ReactElement | null { +function Latency ({ className }: Props): React.ReactElement { const { t } = useTranslation(); const { details, stdDev, timeAvg, timeMax, timeMin } = useLatency(); @@ -109,54 +109,77 @@ function Latency ({ className }: Props): React.ReactElement | null { }), [t] ); - if (details.length <= 2) { - return ( - - ); - } + const isLoaded = details.length > 2; + const EMPTY_TIME = 0.000s; return (
- ('avg')}>{formatTime(timeAvg)} + ('avg')}> + {isLoaded + ? formatTime(timeAvg) + : EMPTY_TIME} + ('std dev')} > - {formatTime(stdDev)} + {isLoaded + ? formatTime(stdDev) + : EMPTY_TIME}
- ('min')}>{formatTime(timeMin)} - ('max')}>{formatTime(timeMax)} + ('min')}> + {isLoaded + ? formatTime(timeMin) + : EMPTY_TIME} + + ('max')}> + {isLoaded + ? formatTime(timeMax) + : EMPTY_TIME + } +
- ('last')}>{formatTime(blockLast, 1)} + ('last')}> + {isLoaded + ? formatTime(blockLast, 1) + : EMPTY_TIME} +
- ('blocktimes (last {{num}} blocks)', { replace: { num: times.labels.length } })} - value={times} - /> - ('blocksize (last {{num}} blocks)', { replace: { num: blocks.labels.length } })} - value={blocks} - /> - ('extrinsics (last {{num}} blocks)', { replace: { num: extrinsics.labels.length } })} - value={extrinsics} - /> - ('events (last {{num}} blocks)', { replace: { num: events.labels.length } })} - value={events} - /> + {isLoaded + ? ( + <> + ('blocktimes (last {{num}} blocks)', { replace: { num: times.labels.length } })} + value={times} + /> + ('blocksize (last {{num}} blocks)', { replace: { num: blocks.labels.length } })} + value={blocks} + /> + ('extrinsics (last {{num}} blocks)', { replace: { num: extrinsics.labels.length } })} + value={extrinsics} + /> + ('events (last {{num}} blocks)', { replace: { num: events.labels.length } })} + value={events} + /> + + ) + : + }
); } diff --git a/packages/page-explorer/src/Latency/useLatency.ts b/packages/page-explorer/src/Latency/useLatency.ts index 34f459fe69..1ab2e97d97 100644 --- a/packages/page-explorer/src/Latency/useLatency.ts +++ b/packages/page-explorer/src/Latency/useLatency.ts @@ -117,7 +117,7 @@ async function getNext (api: ApiPromise, { block: { number: start } }: Detail, { function useLatencyImpl (): Result { const { api } = useApi(); const [details, setDetails] = useState([]); - const signedBlock = useCall(api.derive.chain.subscribeNewBlocks, []); + const signedBlock = useCall(api.derive.chain.subscribeNewBlocks); const hasHistoric = useRef(false); useEffect((): void => { diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 51fb012cc8..99da1b726b 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -48,6 +48,7 @@ export { useLedger } from './useLedger'; export { useLoadingDelay } from './useLoadingDelay'; export { useMapEntries } from './useMapEntries'; export { useMapKeys } from './useMapKeys'; +export { useMemoValue } from './useMemoValue'; export { useModal } from './useModal'; export { useNonEmptyString } from './useNonEmptyString'; export { useNonZeroBn } from './useNonZeroBn'; diff --git a/packages/react-hooks/src/useEventChanges.ts b/packages/react-hooks/src/useEventChanges.ts index c16d1bbe9b..6e3d5d8050 100644 --- a/packages/react-hooks/src/useEventChanges.ts +++ b/packages/react-hooks/src/useEventChanges.ts @@ -13,6 +13,7 @@ import { isFunction } from '@polkadot/util'; import { useApi } from './useApi'; import { useEventTrigger } from './useEventTrigger'; +import { useMemoValue } from './useMemoValue'; export interface Changes { added?: T[]; @@ -54,7 +55,8 @@ function interleave (existing: T[] = [], { added = [], removed export function useEventChanges (checks: EventCheck[], filter: (records: EventRecord[], api: ApiPromise, additional?: A) => Changes, startValue?: T[], additional?: A): T[] | undefined { const { api } = useApi(); const [state, setState] = useState(); - const { blockHash, events } = useEventTrigger(checks); + const memoChecks = useMemoValue(checks); + const { blockHash, events } = useEventTrigger(memoChecks); // when startValue changes, we do a full refresh useEffect((): void => { diff --git a/packages/react-hooks/src/useEventTrigger.ts b/packages/react-hooks/src/useEventTrigger.ts index 339bd4f2fc..5628fcbda3 100644 --- a/packages/react-hooks/src/useEventTrigger.ts +++ b/packages/react-hooks/src/useEventTrigger.ts @@ -11,6 +11,7 @@ import { createNamedHook } from './createNamedHook'; import { useApi } from './useApi'; import { useCall } from './useCall'; import { useIsMountedRef } from './useIsMountedRef'; +import { useMemoValue } from './useMemoValue'; export type EventCheck = AugmentedEvent<'promise'> | false | undefined | null; @@ -26,10 +27,10 @@ const EMPTY_RESULT: Result = { const IDENTITY_FILTER = () => true; -function useEventTriggerImpl (_checks: EventCheck[], filter: (record: EventRecord) => boolean = IDENTITY_FILTER): Result { +function useEventTriggerImpl (checks: EventCheck[], filter: (record: EventRecord) => boolean = IDENTITY_FILTER): Result { const { api } = useApi(); const [state, setState] = useState(() => EMPTY_RESULT); - const [checks] = useState(() => _checks); + const memoChecks = useMemoValue(checks); const mountedRef = useIsMountedRef(); const eventRecords = useCall>(api.query.system.events); @@ -37,7 +38,7 @@ function useEventTriggerImpl (_checks: EventCheck[], filter: (record: EventRecor if (mountedRef.current && eventRecords) { const events = eventRecords.filter((r) => r.event && - checks.some((c) => c && c.is(r.event)) && + memoChecks.some((c) => c && c.is(r.event)) && filter(r) ); @@ -48,7 +49,7 @@ function useEventTriggerImpl (_checks: EventCheck[], filter: (record: EventRecor }); } } - }, [eventRecords, checks, filter, mountedRef]); + }, [eventRecords, filter, memoChecks, mountedRef]); return state; } diff --git a/packages/react-hooks/src/useExtrinsicTrigger.ts b/packages/react-hooks/src/useExtrinsicTrigger.ts index b16e2363b2..a74e094310 100644 --- a/packages/react-hooks/src/useExtrinsicTrigger.ts +++ b/packages/react-hooks/src/useExtrinsicTrigger.ts @@ -10,6 +10,7 @@ import { createNamedHook } from './createNamedHook'; import { useApi } from './useApi'; import { useCall } from './useCall'; import { useIsMountedRef } from './useIsMountedRef'; +import { useMemoValue } from './useMemoValue'; type ExtrinsicCheck = SubmittableExtrinsicFunction<'promise'> | false | undefined | null; @@ -17,14 +18,15 @@ function useExtrinsicTriggerImpl (checks: ExtrinsicCheck[]): string { const { api } = useApi(); const [trigger, setTrigger] = useState('0'); const mountedRef = useIsMountedRef(); + const memoChecks = useMemoValue(checks); const block = useCall(api.derive.chain.subscribeNewBlocks); useEffect((): void => { mountedRef.current && block && block.extrinsics && block.extrinsics.filter(({ extrinsic }) => extrinsic && - checks.some((c) => c && c.is(extrinsic)) + memoChecks.some((c) => c && c.is(extrinsic)) ).length && setTrigger(() => block.createdAtHash?.toHex() || ''); - }, [block, checks, mountedRef]); + }, [block, memoChecks, mountedRef]); return trigger; } diff --git a/packages/react-hooks/src/useMemoValue.spec.ts b/packages/react-hooks/src/useMemoValue.spec.ts new file mode 100644 index 0000000000..a0b3537de6 --- /dev/null +++ b/packages/react-hooks/src/useMemoValue.spec.ts @@ -0,0 +1,60 @@ +// Copyright 2017-2023 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { isDifferent } from './useMemoValue'; + +describe('useMemoValue', (): void => { + describe('isDifferent', (): void => { + it('works on straight references', (): void => { + expect(isDifferent(1, 2)).toEqual(true); + expect(isDifferent(null, false)).toEqual(true); + expect(isDifferent({}, [])).toEqual(true); + expect(isDifferent(2, 2)).toEqual(false); + }); + + it('compares flat arrays', (): void => { + expect(isDifferent([1, 2, 3], [1, 2, 3])).toEqual(false); + expect(isDifferent([1, 2, 3], [4, 5, 6])).toEqual(true); + expect(isDifferent([1, 2, 3, 4], [1, 2, 3])).toEqual(true); + }); + + it('compares 1-level-nested arrays', (): void => { + expect(isDifferent( + [1, [2, 3]], + [1, [2, 3]] + )).toEqual(false); + expect(isDifferent( + [1, [2, 3]], + [1, [2]] + )).toEqual(true); + expect(isDifferent( + [1, [2, 3]], + [1, [3, 2]] + )).toEqual(true); + expect(isDifferent( + [1, [2, 3], 4], + [1, [2, 3], 4] + )).toEqual(false); + }); + + it('compares 2-level-nested arrays', (): void => { + const V34 = [3, 4]; + + // this one is same since the [3, 4] compares by ref + expect(isDifferent( + [1, [2, V34]], + [1, [2, V34]] + )).toEqual(false); + // this one is different since the [3, 4] is at level 2 + expect(isDifferent( + [1, [2, V34]], + [1, [2, [3, 4]]] + )).toEqual(true); + // this one is different since the [3, 4] is at level 2 + expect(isDifferent( + [1, [2, [3, 4]]], + [1, [2, [3, 4]]] + )).toEqual(true); + }); + }); +}); diff --git a/packages/react-hooks/src/useMemoValue.ts b/packages/react-hooks/src/useMemoValue.ts new file mode 100644 index 0000000000..208717c311 --- /dev/null +++ b/packages/react-hooks/src/useMemoValue.ts @@ -0,0 +1,70 @@ +// Copyright 2017-2023 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useMemo, useRef } from 'react'; + +import { stringify } from '@polkadot/util'; + +interface State { + stringified: string; + value: T; +} + +/** + * @internal + * + * Does a check between two values to determine if they are equivalent. For + * non-array values it does a simple === compare, for arrays is checks the + * lengths and the individual items. (This is limited to a depth) + */ +export function isDifferent (a: unknown, b: unknown, depth = -1): boolean { + // increase the depth for arrays (we start at -1, so 0 is top-level) + depth++; + + // check the actual value references for an exact match + return a !== b + // check if both are arrays with matching length + ? depth < 2 && Array.isArray(a) && Array.isArray(b) && a.length === b.length + // check for any differences inside the arrays (with depth) + ? a.some((ai, i) => isDifferent(ai, b[i], depth)) + // not equal and not an array + : true + // straigh match, exact object found + : false; +} + +/** + * @internal + * + * Checks the supplied value against the previous state, returning either the + * previous state (if we have a match) or a new object for future compares. + **/ +export function getMemoValue (ref: { current: State | null }, value: T): T { + let curr = ref.current; + + // check that either we have no previous or the value changed + if (!curr || isDifferent(curr.value, value)) { + const stringified = stringify({ value }); + + // no previous or the stringified result is different + if (!curr || curr.stringified !== stringified) { + curr = { stringified, value }; + } + } + + if (ref.current !== curr) { + ref.current = curr; + } + + return ref.current.value; +} + +// NOTE: Generic, cannot be used in named hook +export function useMemoValue (value: T): T { + const ref = useRef | null>(null); + + return useMemo( + () => getMemoValue(ref, value), + [ref, value] + ); +}