Add useIsMountedRef hook (#2041)

* Add useIsMountedRef hook

* Use mounted ref on async operations (staking)

* Rework historic caching

* linting
This commit is contained in:
Jaco Greeff 2019-12-13 17:22:14 +01:00 committed by GitHub
parent 4988702054
commit 74dbf64808
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 92 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -21,6 +21,7 @@ export interface BareProps {
export interface AppProps {
basePath: string;
className?: string;
onStatusChange: (status: ActionStatus) => void;
}

View File

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

View File

@ -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 => {

View File

@ -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 => {

View File

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

View File

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

View 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;
}