Pull in useMemoValue (& basic use) (#8750)
This commit is contained in:
parent
56d87e6653
commit
ba86c6a111
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
60
packages/react-hooks/src/useMemoValue.spec.ts
Normal file
60
packages/react-hooks/src/useMemoValue.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
70
packages/react-hooks/src/useMemoValue.ts
Normal file
70
packages/react-hooks/src/useMemoValue.ts
Normal 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]
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user