Pull in useMemoValue (& basic use) (#8750)

This commit is contained in:
Jaco 2023-01-10 14:09:16 +02:00 committed by GitHub
parent 56d87e6653
commit ba86c6a111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 202 additions and 43 deletions

View File

@ -91,7 +91,7 @@ function formatTime (time: number, divisor = 1000): string {
return `${(time / divisor).toFixed(3)}s`;
}
function Latency ({ className }: Props): React.ReactElement<Props> | null {
function Latency ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { details, stdDev, timeAvg, timeMax, timeMin } = useLatency();
@ -109,54 +109,77 @@ function Latency ({ className }: Props): React.ReactElement<Props> | null {
}), [t]
);
if (details.length <= 2) {
return (
<Spinner />
);
}
const isLoaded = details.length > 2;
const EMPTY_TIME = <span className='--tmp'>0.000s</span>;
return (
<div className={className}>
<SummaryBox>
<section>
<CardSummary label={t<string>('avg')}>{formatTime(timeAvg)}</CardSummary>
<CardSummary label={t<string>('avg')}>
{isLoaded
? formatTime(timeAvg)
: EMPTY_TIME}
</CardSummary>
<CardSummary
className='media--1000'
label={t<string>('std dev')}
>
{formatTime(stdDev)}
{isLoaded
? formatTime(stdDev)
: EMPTY_TIME}
</CardSummary>
</section>
<section>
<CardSummary label={t<string>('min')}>{formatTime(timeMin)}</CardSummary>
<CardSummary label={t<string>('max')}>{formatTime(timeMax)}</CardSummary>
<CardSummary label={t<string>('min')}>
{isLoaded
? formatTime(timeMin)
: EMPTY_TIME}
</CardSummary>
<CardSummary label={t<string>('max')}>
{isLoaded
? formatTime(timeMax)
: EMPTY_TIME
}
</CardSummary>
</section>
<CardSummary label={t<string>('last')}>{formatTime(blockLast, 1)}</CardSummary>
<CardSummary label={t<string>('last')}>
{isLoaded
? formatTime(blockLast, 1)
: EMPTY_TIME}
</CardSummary>
</SummaryBox>
<Chart
colors={COLORS_TIMES}
legends={timesLegend}
title={t<string>('blocktimes (last {{num}} blocks)', { replace: { num: times.labels.length } })}
value={times}
/>
<Chart
colors={COLORS_BLOCKS}
legends={blocksLegend}
title={t<string>('blocksize (last {{num}} blocks)', { replace: { num: blocks.labels.length } })}
value={blocks}
/>
<Chart
colors={COLORS_TXS}
legends={extrinsicsLegend}
title={t<string>('extrinsics (last {{num}} blocks)', { replace: { num: extrinsics.labels.length } })}
value={extrinsics}
/>
<Chart
colors={COLORS_EVENTS}
legends={eventsLegend}
title={t<string>('events (last {{num}} blocks)', { replace: { num: events.labels.length } })}
value={events}
/>
{isLoaded
? (
<>
<Chart
colors={COLORS_TIMES}
legends={timesLegend}
title={t<string>('blocktimes (last {{num}} blocks)', { replace: { num: times.labels.length } })}
value={times}
/>
<Chart
colors={COLORS_BLOCKS}
legends={blocksLegend}
title={t<string>('blocksize (last {{num}} blocks)', { replace: { num: blocks.labels.length } })}
value={blocks}
/>
<Chart
colors={COLORS_TXS}
legends={extrinsicsLegend}
title={t<string>('extrinsics (last {{num}} blocks)', { replace: { num: extrinsics.labels.length } })}
value={extrinsics}
/>
<Chart
colors={COLORS_EVENTS}
legends={eventsLegend}
title={t<string>('events (last {{num}} blocks)', { replace: { num: events.labels.length } })}
value={events}
/>
</>
)
: <Spinner />
}
</div>
);
}

View File

@ -117,7 +117,7 @@ async function getNext (api: ApiPromise, { block: { number: start } }: Detail, {
function useLatencyImpl (): Result {
const { api } = useApi();
const [details, setDetails] = useState<Detail[]>([]);
const signedBlock = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks, []);
const signedBlock = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks);
const hasHistoric = useRef(false);
useEffect((): void => {

View File

@ -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';

View File

@ -13,6 +13,7 @@ import { isFunction } from '@polkadot/util';
import { useApi } from './useApi';
import { useEventTrigger } from './useEventTrigger';
import { useMemoValue } from './useMemoValue';
export interface Changes<T extends Codec> {
added?: T[];
@ -54,7 +55,8 @@ function interleave <T extends Codec> (existing: T[] = [], { added = [], removed
export function useEventChanges <T extends Codec, A> (checks: EventCheck[], filter: (records: EventRecord[], api: ApiPromise, additional?: A) => Changes<T>, startValue?: T[], additional?: A): T[] | undefined {
const { api } = useApi();
const [state, setState] = useState<T[] | undefined>();
const { blockHash, events } = useEventTrigger(checks);
const memoChecks = useMemoValue(checks);
const { blockHash, events } = useEventTrigger(memoChecks);
// when startValue changes, we do a full refresh
useEffect((): void => {

View File

@ -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<Vec<EventRecord>>(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;
}

View File

@ -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<SignedBlockExtended>(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;
}

View File

@ -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);
});
});
});

View File

@ -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<T> {
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 <T> (ref: { current: State<T> | 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 <T> (value: T): T {
const ref = useRef<State<T> | null>(null);
return useMemo(
() => getMemoValue(ref, value),
[ref, value]
);
}