Add useIsMountedRef hook (#2041)
* Add useIsMountedRef hook * Use mounted ref on async operations (staking) * Rework historic caching * linting
This commit is contained in:
parent
4988702054
commit
74dbf64808
@ -2,14 +2,10 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import {
|
||||
withTranslation,
|
||||
useTranslation as _useTranslation,
|
||||
UseTranslationResponse
|
||||
} from 'react-i18next';
|
||||
import { useTranslation as useTranslationBase, UseTranslationResponse, withTranslation } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): UseTranslationResponse {
|
||||
return _useTranslation('app-accounts');
|
||||
return useTranslationBase('app-accounts');
|
||||
}
|
||||
|
||||
export default withTranslation(['app-accounts']);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { DerivedHeartbeats, DerivedStakingOverview } from '@polkadot/api-derive/types';
|
||||
import { AppProps, I18nProps } from '@polkadot/react-components/types';
|
||||
import { AppProps as Props } from '@polkadot/react-components/types';
|
||||
import { AccountId } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@ -22,12 +22,9 @@ import Summary from './Overview/Summary';
|
||||
import Query from './Query';
|
||||
import Targets from './Targets';
|
||||
import { MAX_SESSIONS } from './constants';
|
||||
import translate from './translate';
|
||||
import { useTranslation } from './translate';
|
||||
import useSessionRewards from './useSessionRewards';
|
||||
|
||||
interface Props extends AppProps, I18nProps {
|
||||
}
|
||||
|
||||
const EMPY_ACCOUNTS: string[] = [];
|
||||
const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
|
||||
|
||||
@ -40,7 +37,8 @@ function transformStakingControllers ([stashes, controllers]: [AccountId[], Opti
|
||||
];
|
||||
}
|
||||
|
||||
function StakingApp ({ basePath, className, t }: Props): React.ReactElement<Props> {
|
||||
function StakingApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api, isSubstrateV2 } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const { pathname } = useLocation();
|
||||
@ -135,22 +133,20 @@ function StakingApp ({ basePath, className, t }: Props): React.ReactElement<Prop
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(
|
||||
styled(StakingApp)`
|
||||
.staking--hidden {
|
||||
display: none;
|
||||
}
|
||||
export default styled(StakingApp)`
|
||||
.staking--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.staking--queryInput {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.staking--queryInput {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.staking--Chart h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.staking--Chart h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.staking--Chart+.staking--Chart {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`
|
||||
);
|
||||
.staking--Chart+.staking--Chart {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { useTranslation as useTranslationBase, UseTranslationResponse, withTranslation } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): UseTranslationResponse {
|
||||
return useTranslationBase('app-staking');
|
||||
}
|
||||
|
||||
export default withTranslation(['app-staking']);
|
||||
|
||||
@ -6,18 +6,19 @@ import { SessionIndex } from '@polkadot/types/interfaces';
|
||||
import { SessionRewards } from './types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks';
|
||||
import { u32 } from '@polkadot/types';
|
||||
|
||||
export default function useBlockCounts (accountId: string, sessionRewards: SessionRewards[]): u32[] {
|
||||
const { api } = useApi();
|
||||
const mounted = useIsMountedRef();
|
||||
const [counts, setCounts] = useState<u32[]>([]);
|
||||
const [historic, setHistoric] = useState<u32[]>([]);
|
||||
const sessionIndex = useCall<SessionIndex>(api.query.session.currentIndex, []);
|
||||
const current = useCall<u32>(api.query.imOnline?.authoredBlocks, [sessionIndex, accountId]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (api.query.imOnline?.authoredBlocks && sessionRewards && sessionRewards.length) {
|
||||
if (api.query.imOnline?.authoredBlocks && sessionRewards?.length) {
|
||||
const filtered = sessionRewards.filter(({ sessionIndex }): boolean => sessionIndex.gtn(0));
|
||||
|
||||
if (filtered.length) {
|
||||
@ -25,7 +26,9 @@ export default function useBlockCounts (accountId: string, sessionRewards: Sessi
|
||||
.all(filtered.map(({ parentHash, sessionIndex }): Promise<u32> =>
|
||||
api.query.imOnline.authoredBlocks.at(parentHash, sessionIndex.subn(1), accountId) as Promise<u32>
|
||||
))
|
||||
.then(setHistoric);
|
||||
.then((historic): void => {
|
||||
mounted.current && setHistoric(historic);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [accountId, sessionRewards]);
|
||||
|
||||
@ -9,7 +9,7 @@ import BN from 'bn.js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import { registry } from '@polkadot/react-api';
|
||||
import { useApi, useCacheKey } from '@polkadot/react-hooks';
|
||||
import { useApi, useCacheKey, useIsMountedRef } from '@polkadot/react-hooks';
|
||||
import { createType } from '@polkadot/types';
|
||||
import { bnMax, u8aToU8a } from '@polkadot/util';
|
||||
|
||||
@ -32,8 +32,8 @@ interface Serialized {
|
||||
const MAX_BLOCKS = 2500;
|
||||
|
||||
function fromJSON (sessions: Serialized[]): SessionRewards[] {
|
||||
let hasSome = false;
|
||||
let keepAll = true;
|
||||
let hasData = false;
|
||||
let keepAll = false;
|
||||
|
||||
return sessions
|
||||
.map(({ blockHash, blockNumber, isEventsEmpty, parentHash, reward, sessionIndex, slashes, treasury }): SessionRewards => ({
|
||||
@ -50,17 +50,22 @@ function fromJSON (sessions: Serialized[]): SessionRewards[] {
|
||||
treasury: createType(registry, 'Balance', treasury)
|
||||
}))
|
||||
.filter(({ parentHash }): boolean => !parentHash.isEmpty)
|
||||
.filter(({ isEventsEmpty }): boolean => {
|
||||
if (!isEventsEmpty) {
|
||||
// we first see if we have some data up to this point (we may not, i.e. non-archive)
|
||||
hasSome = true;
|
||||
} else if (hasSome) {
|
||||
// if data is followed by empty, drop everything from here on
|
||||
keepAll = false;
|
||||
.reverse()
|
||||
// we drop everything before the second non-empty
|
||||
.filter(({ isEventsEmpty }, index): boolean => {
|
||||
if (index !== 0) {
|
||||
if (!isEventsEmpty) {
|
||||
// we first see if we have some data up to this point (we may not, i.e. non-archive)
|
||||
hasData = true;
|
||||
} else if (hasData) {
|
||||
// if data is followed by empty, drop everything from here on
|
||||
keepAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
return keepAll;
|
||||
});
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
||||
function toJSON (sessions: SessionRewards[], maxSessions: number): Serialized[] {
|
||||
@ -141,6 +146,7 @@ async function loadSome (api: ApiPromise, fromHash: Hash, toHash: Hash): Promise
|
||||
|
||||
export default function useSessionRewards (maxSessions: number): SessionRewards[] {
|
||||
const { api } = useApi();
|
||||
const mounted = useIsMountedRef();
|
||||
const [getCache, setCache] = useCacheKey<Serialized[]>('hooks:sessionSlashes');
|
||||
const [filtered, setFiltered] = useState<SessionRewards[]>([]);
|
||||
|
||||
@ -169,12 +175,14 @@ export default function useSessionRewards (maxSessions: number): SessionRewards[
|
||||
toNumber = fromNumber;
|
||||
fromNumber = bnMax(toNumber.subn(MAX_BLOCKS), new BN(1));
|
||||
|
||||
setCache(toJSON(workQueue, maxSessionsStore));
|
||||
setFiltered(workQueue.slice(-maxSessions));
|
||||
if (mounted.current) {
|
||||
setCache(toJSON(workQueue, maxSessionsStore));
|
||||
setFiltered(workQueue.slice(-maxSessions));
|
||||
}
|
||||
|
||||
const lastNumber = workQueue[workQueue.length - 1]?.blockNumber;
|
||||
|
||||
if (!lastNumber || fromNumber.eqn(1) || ((workQueue.length >= maxSessionsStore) && fromNumber.lt(savedNumber || lastNumber))) {
|
||||
if (!mounted.current || !lastNumber || fromNumber.eqn(1) || ((workQueue.length >= maxSessionsStore) && fromNumber.lt(savedNumber || lastNumber))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { TokenUnit } from '@polkadot/react-components/InputNumber';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
import ApiSigner from '@polkadot/react-signer/ApiSigner';
|
||||
import { u32 as U32 } from '@polkadot/types';
|
||||
import { createType } from '@polkadot/types';
|
||||
import { formatBalance, isTestChain } from '@polkadot/util';
|
||||
import addressDefaults from '@polkadot/util-crypto/address/defaults';
|
||||
|
||||
@ -43,9 +43,8 @@ interface InjectedAccountExt {
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_DECIMALS = new U32(registry, 12);
|
||||
const DEFAULT_SS58 = new U32(registry, addressDefaults.prefix);
|
||||
|
||||
const DEFAULT_DECIMALS = createType(registry, 'u32', 12);
|
||||
const DEFAULT_SS58 = createType(registry, 'u32', addressDefaults.prefix);
|
||||
const injectedPromise = web3Enable('polkadot-js/apps');
|
||||
let api: ApiPromise;
|
||||
|
||||
|
||||
@ -11,11 +11,11 @@ const defaultState: Partial<QueueProps> = {
|
||||
txqueue: [] as QueueTx[]
|
||||
};
|
||||
|
||||
const Context: React.Context<QueueProps> = React.createContext<QueueProps>(defaultState as QueueProps);
|
||||
const QueueConsumer: React.Consumer<QueueProps> = Context.Consumer;
|
||||
const QueueProvider: React.Provider<QueueProps> = Context.Provider;
|
||||
const StatusContext: React.Context<QueueProps> = React.createContext<QueueProps>(defaultState as QueueProps);
|
||||
const QueueConsumer: React.Consumer<QueueProps> = StatusContext.Consumer;
|
||||
const QueueProvider: React.Provider<QueueProps> = StatusContext.Provider;
|
||||
|
||||
export default Context;
|
||||
export default StatusContext;
|
||||
|
||||
export {
|
||||
QueueConsumer,
|
||||
|
||||
@ -21,6 +21,7 @@ export interface BareProps {
|
||||
|
||||
export interface AppProps {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
onStatusChange: (status: ActionStatus) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ export { default as useCall } from './useCall';
|
||||
export { default as useForm } from './useForm';
|
||||
export { default as useDebounce } from './useDebounce';
|
||||
export { default as useFavorites } from './useFavorites';
|
||||
export { default as useIsMountedRef } from './useIsMountedRef';
|
||||
export { default as usePassword } from './usePassword';
|
||||
export { default as useToggle } from './useToggle';
|
||||
export { default as useTx } from './useTx';
|
||||
|
||||
@ -5,20 +5,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
|
||||
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
|
||||
interface UseAccounts {
|
||||
allAccounts: string[];
|
||||
hasAccounts: boolean;
|
||||
}
|
||||
|
||||
export default function useAccounts (): UseAccounts {
|
||||
const mounted = useIsMountedRef();
|
||||
const [state, setState] = useState<UseAccounts>({ allAccounts: [], hasAccounts: false });
|
||||
|
||||
useEffect((): () => void => {
|
||||
const subscription = accountObservable.subject.subscribe((accounts): void => {
|
||||
const allAccounts = accounts ? Object.keys(accounts) : [];
|
||||
const hasAccounts = allAccounts.length !== 0;
|
||||
if (mounted.current) {
|
||||
const allAccounts = accounts ? Object.keys(accounts) : [];
|
||||
const hasAccounts = allAccounts.length !== 0;
|
||||
|
||||
setState({ allAccounts, hasAccounts });
|
||||
setState({ allAccounts, hasAccounts });
|
||||
}
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
|
||||
@ -5,20 +5,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import addressObservable from '@polkadot/ui-keyring/observable/addresses';
|
||||
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
|
||||
interface UseAccounts {
|
||||
allAddresses: string[];
|
||||
hasAddresses: boolean;
|
||||
}
|
||||
|
||||
export default function useAccounts (): UseAccounts {
|
||||
const mounted = useIsMountedRef();
|
||||
const [state, setState] = useState<UseAccounts>({ allAddresses: [], hasAddresses: false });
|
||||
|
||||
useEffect((): () => void => {
|
||||
const subscription = addressObservable.subject.subscribe((addresses): void => {
|
||||
const allAddresses = addresses ? Object.keys(addresses) : [];
|
||||
const hasAddresses = allAddresses.length !== 0;
|
||||
if (mounted.current) {
|
||||
const allAddresses = addresses ? Object.keys(addresses) : [];
|
||||
const hasAddresses = allAddresses.length !== 0;
|
||||
|
||||
setState({ allAddresses, hasAddresses });
|
||||
setState({ allAddresses, hasAddresses });
|
||||
}
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
|
||||
@ -8,6 +8,8 @@ import { CallOptions, CallParam, CallParams } from './types';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { isNull, isUndefined } from '@polkadot/util';
|
||||
|
||||
import useIsMountedRef, { MountedRef } from './useIsMountedRef';
|
||||
|
||||
interface TrackFnCallback {
|
||||
(value: Codec): void;
|
||||
}
|
||||
@ -29,7 +31,6 @@ interface TrackFn {
|
||||
interface Tracker {
|
||||
isActive: boolean;
|
||||
count: number;
|
||||
paramCount: number;
|
||||
serialized: string | null;
|
||||
subscriber: TrackFnResult | null;
|
||||
}
|
||||
@ -64,29 +65,31 @@ function unsubscribe (tracker: TrackerRef): void {
|
||||
}
|
||||
|
||||
// subscribe, tyring to play nice with the browser threads
|
||||
function subscribe <T> (tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity }: CallOptions<T>): void {
|
||||
function subscribe <T> (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity }: CallOptions<T>): void {
|
||||
const validParams = params.filter((p): boolean => !isUndefined(p));
|
||||
|
||||
unsubscribe(tracker);
|
||||
|
||||
setTimeout((): void => {
|
||||
if (fn && (!fn.meta || !fn.meta.type?.isDoubleMap || validParams.length === 2)) {
|
||||
// swap to acive mode and reset our count
|
||||
tracker.current.isActive = true;
|
||||
tracker.current.count = 0;
|
||||
if (mounted.current) {
|
||||
if (fn && (!fn.meta || !fn.meta.type?.isDoubleMap || validParams.length === 2)) {
|
||||
// swap to acive mode and reset our count
|
||||
tracker.current.isActive = true;
|
||||
tracker.current.count = 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore We tried to get the typings right, close but no cigar...
|
||||
tracker.current.subscriber = fn(...params, (value: any): void => {
|
||||
// when we don't have an active sub, or single-shot, ignore (we use the isActive flag here
|
||||
// since .subscriber may not be set on immeditae callback)
|
||||
if (tracker.current.isActive && (!isSingle || !tracker.current.count)) {
|
||||
tracker.current.count++;
|
||||
setValue(transform(value));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tracker.current.subscriber = null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore We tried to get the typings right, close but no cigar...
|
||||
tracker.current.subscriber = fn(...params, (value: any): void => {
|
||||
// when we don't have an active sub, or single-shot, ignore (we use the isActive flag here
|
||||
// since .subscriber may not be set on immeditae callback)
|
||||
if (mounted.current && tracker.current.isActive && (!isSingle || !tracker.current.count)) {
|
||||
tracker.current.count++;
|
||||
setValue(transform(value));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tracker.current.subscriber = null;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@ -96,8 +99,9 @@ function subscribe <T> (tracker: TrackerRef, fn: TrackFn | undefined, params: Ca
|
||||
// - has a callback to set the value
|
||||
// FIXME The typings here need some serious TLC
|
||||
export default function useCall <T> (fn: TrackFn | undefined, params: CallParams, options: CallOptions<T> = {}): T | undefined {
|
||||
const mounted = useIsMountedRef();
|
||||
const tracker = useRef<Tracker>({ isActive: false, count: 0, serialized: null, subscriber: null });
|
||||
const [value, setValue] = useState<T | undefined>(options.defaultValue);
|
||||
const tracker = useRef<Tracker>({ isActive: false, count: 0, paramCount: -1, serialized: null, subscriber: null });
|
||||
|
||||
// initial effect, we need an unsubscription
|
||||
useEffect((): () => void => {
|
||||
@ -108,20 +112,14 @@ export default function useCall <T> (fn: TrackFn | undefined, params: CallParams
|
||||
|
||||
// on changes, re-subscribe
|
||||
useEffect((): void => {
|
||||
// check if we have a function
|
||||
if (fn) {
|
||||
const validParams = params.filter((param): boolean => !isUndefined(param));
|
||||
// check if we have a function & that we are mounted
|
||||
if (mounted.current && fn) {
|
||||
const [serialized, mappedParams] = extractParams(fn, params, options.paramMap || transformIdentity);
|
||||
|
||||
// in the case on unmounting, the params may go empty, cater for this, don't trigger
|
||||
if (validParams.length >= tracker.current.paramCount) {
|
||||
const [serialized, mappedParams] = extractParams(fn, params, options.paramMap || transformIdentity);
|
||||
if (mappedParams && serialized !== tracker.current.serialized) {
|
||||
tracker.current.serialized = serialized;
|
||||
|
||||
if (mappedParams && serialized !== tracker.current.serialized) {
|
||||
tracker.current.paramCount = validParams.length;
|
||||
tracker.current.serialized = serialized;
|
||||
|
||||
subscribe(tracker, fn, mappedParams, setValue, options);
|
||||
}
|
||||
subscribe(mounted, tracker, fn, mappedParams, setValue, options);
|
||||
}
|
||||
}
|
||||
}, [fn, params]);
|
||||
|
||||
@ -4,13 +4,16 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
|
||||
// Debounces inputs
|
||||
export default function useDebounce <T> (value: T, delay = 250): T {
|
||||
const mounted = useIsMountedRef();
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
mounted.current && setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// each time it renders, it clears
|
||||
|
||||
23
packages/react-hooks/src/useIsMountedRef.tsx
Normal file
23
packages/react-hooks/src/useIsMountedRef.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2017-2019 @polkadot/react-hooks authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export interface MountedRef {
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export default function useIsMountedRef (): MountedRef {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect((): () => void => {
|
||||
isMounted.current = true;
|
||||
|
||||
return (): void => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMounted;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user