Browse Source

useMemo for table headers/footers/filters (#2601)

* useMemo for table headers/footers/filters

* contacts mishap
Jaco Greeff 4 years ago
parent
commit
284f7a667d
28 changed files with 362 additions and 288 deletions
  1. 38 32
      packages/page-accounts/src/Accounts/index.tsx
  2. 25 21
      packages/page-accounts/src/Contacts/index.tsx
  3. 15 13
      packages/page-accounts/src/Vanity/index.tsx
  4. 11 9
      packages/page-council/src/Motions/index.tsx
  5. 13 9
      packages/page-council/src/Overview/Candidates.tsx
  6. 7 5
      packages/page-council/src/Overview/Members.tsx
  7. 8 6
      packages/page-democracy/src/Overview/DispatchQueue.tsx
  8. 9 7
      packages/page-democracy/src/Overview/Externals.tsx
  9. 9 7
      packages/page-democracy/src/Overview/Proposals.tsx
  10. 11 9
      packages/page-democracy/src/Overview/Referendums.tsx
  11. 6 4
      packages/page-explorer/src/BlockHeaders.tsx
  12. 8 6
      packages/page-explorer/src/BlockInfo/Extrinsics.tsx
  13. 4 2
      packages/page-explorer/src/BlockInfo/Logs.tsx
  14. 6 4
      packages/page-explorer/src/Events.tsx
  15. 9 7
      packages/page-explorer/src/NodeInfo/Peers.tsx
  16. 8 8
      packages/page-parachains/src/Overview/Parachains.tsx
  17. 8 6
      packages/page-society/src/Overview/Bids.tsx
  18. 9 7
      packages/page-society/src/Overview/Candidates.tsx
  19. 8 6
      packages/page-society/src/Overview/Defender.tsx
  20. 7 5
      packages/page-society/src/Overview/Members.tsx
  21. 21 17
      packages/page-staking/src/Actions/index.tsx
  22. 31 25
      packages/page-staking/src/Overview/CurrentList.tsx
  23. 30 24
      packages/page-staking/src/Payouts/index.tsx
  24. 24 20
      packages/page-staking/src/Targets/index.tsx
  25. 6 4
      packages/page-tech-comm/src/Overview/Members.tsx
  26. 10 8
      packages/page-tech-comm/src/Proposals/index.tsx
  27. 10 8
      packages/page-treasury/src/Overview/Proposals.tsx
  28. 11 9
      packages/page-treasury/src/Overview/Tips.tsx

+ 38 - 32
packages/page-accounts/src/Accounts/index.tsx

@@ -7,7 +7,7 @@ import { ComponentProps as Props } from '../types';
 import { SortedAccount } from './types';
 
 import BN from 'bn.js';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import styled from 'styled-components';
 import keyring from '@polkadot/ui-keyring';
 import { getLedger, isLedger } from '@polkadot/react-api';
@@ -98,7 +98,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
   const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
   const [{ balanceTotal }, setBalances] = useState<Balances>({ accounts: {} });
   const [sortedAccounts, setSortedAccounts] = useState<SortedAccount[]>([]);
-  const [filter, setFilter] = useState<string>('');
+  const [filterOn, setFilter] = useState<string>('');
 
   useEffect((): void => {
     setSortedAccounts(
@@ -119,6 +119,38 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
     []
   );
 
+  const header = useMemo(() => [
+    [t('accounts'), 'start', 3],
+    [t('parent'), 'address'],
+    [t('type')],
+    [t('tags'), 'start'],
+    [t('transactions')],
+    [t('balances')],
+    [undefined, undefined, 2]
+  ], [t]);
+
+  const footer = useMemo(() => (
+    <tr>
+      <td colSpan={7} />
+      <td className='number'>
+        {balanceTotal && <FormatBalance value={balanceTotal} />}
+      </td>
+      <td colSpan={2} />
+    </tr>
+  ), [balanceTotal]);
+
+  const filter = useMemo(() => (
+    <div className='filter--tags'>
+      <Input
+        autoFocus
+        isFull
+        label={t('filter by name or tags')}
+        onChange={setFilter}
+        value={filterOn}
+      />
+    </div>
+  ), [filterOn, t]);
+
   return (
     <div className={className}>
       <Banner />
@@ -171,40 +203,14 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
       </Button.Group>
       <Table
         empty={t('no accounts yet, create or import an existing')}
-        filter={
-          <div className='filter--tags'>
-            <Input
-              autoFocus
-              isFull
-              label={t('filter by name or tags')}
-              onChange={setFilter}
-              value={filter}
-            />
-          </div>
-        }
-        footer={
-          <tr>
-            <td colSpan={7} />
-            <td className='number'>
-              {balanceTotal && <FormatBalance value={balanceTotal} />}
-            </td>
-            <td colSpan={2} />
-          </tr>
-        }
-        header={[
-          [t('accounts'), 'start', 3],
-          [t('parent'), 'address'],
-          [t('type')],
-          [t('tags'), 'start'],
-          [t('transactions')],
-          [t('balances')],
-          [undefined, undefined, 2]
-        ]}
+        filter={filter}
+        footer={footer}
+        header={header}
       >
         {sortedAccounts.map(({ account, isFavorite }): React.ReactNode => (
           <Account
             account={account}
-            filter={filter}
+            filter={filterOn}
             isFavorite={isFavorite}
             key={account.address}
             setBalance={_setBalance}

+ 25 - 21
packages/page-accounts/src/Contacts/index.tsx

@@ -4,7 +4,7 @@
 
 import { ComponentProps as Props } from '../types';
 
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import styled from 'styled-components';
 import { Button, Input, Table } from '@polkadot/react-components';
 import { useAddresses, useFavorites, useToggle } from '@polkadot/react-hooks';
@@ -23,7 +23,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
   const [isCreateOpen, toggleCreate] = useToggle(false);
   const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
   const [sortedAddresses, setSortedAddresses] = useState<SortedAddress[]>([]);
-  const [filter, setFilter] = useState<string>('');
+  const [filterOn, setFilter] = useState<string>('');
 
   useEffect((): void => {
     setSortedAddresses(
@@ -39,6 +39,26 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
     );
   }, [allAddresses, favorites]);
 
+  const header = useMemo(() => [
+    [t('contacts'), 'start', 2],
+    [t('tags'), 'start'],
+    [t('transactions')],
+    [t('balances')],
+    [undefined, undefined, 2]
+  ], [t]);
+
+  const filter = useMemo(() => (
+    <div className='filter--tags'>
+      <Input
+        autoFocus
+        isFull
+        label={t('filter by name or tags')}
+        onChange={setFilter}
+        value={filterOn}
+      />
+    </div>
+  ), [filterOn, t]);
+
   return (
     <div className={className}>
       <Button.Group>
@@ -56,29 +76,13 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
       )}
       <Table
         empty={t('no addresses saved yet, add any existing address')}
-        filter={
-          <div className='filter--tags'>
-            <Input
-              autoFocus
-              isFull
-              label={t('filter by name or tags')}
-              onChange={setFilter}
-              value={filter}
-            />
-          </div>
-        }
-        header={[
-          [t('contacts'), 'start', 2],
-          [t('tags'), 'start'],
-          [t('transactions')],
-          [t('balances')],
-          [undefined, undefined, 2]
-        ]}
+        filter={filter}
+        header={header}
       >
         {sortedAddresses.map(({ address, isFavorite }): React.ReactNode => (
           <Address
             address={address}
-            filter={filter}
+            filter={filterOn}
             isFavorite={isFavorite}
             key={address}
             toggleFavorite={toggleFavorite}

+ 15 - 13
packages/page-accounts/src/Vanity/index.tsx

@@ -6,7 +6,7 @@ import { KeypairType } from '@polkadot/util-crypto/types';
 import { GeneratorMatches, GeneratorMatch, GeneratorResult } from '@polkadot/vanitygen/types';
 import { ComponentProps as Props } from '../types';
 
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import styled from 'styled-components';
 import { Button, Dropdown, Input, Table } from '@polkadot/react-components';
 import { useIsMountedRef } from '@polkadot/react-hooks';
@@ -163,6 +163,19 @@ function VanityApp ({ className, onStatusChange }: Props): React.ReactElement<Pr
     }
   }, [_executeGeneration, isRunning]);
 
+  const header = useMemo(() => [
+    [t('matches'), 'start', 2],
+    [t('Evaluated {{count}} keys in {{elapsed}}s ({{avg}} keys/s)', {
+      replace: {
+        avg: (keyCount / (elapsed / 1000)).toFixed(3),
+        count: keyCount,
+        elapsed: (elapsed / 1000).toFixed(2)
+      }
+    }), 'start'],
+    [t('secret'), 'start'],
+    []
+  ], [elapsed, keyCount, t]);
+
   return (
     <div className={className}>
       <div className='ui--row'>
@@ -217,18 +230,7 @@ function VanityApp ({ className, onStatusChange }: Props): React.ReactElement<Pr
         <Table
           className='vanity--App-matches'
           empty={t('No matches found')}
-          header={[
-            [t('matches'), 'start', 2],
-            [t('Evaluated {{count}} keys in {{elapsed}}s ({{avg}} keys/s)', {
-              replace: {
-                avg: (keyCount / (elapsed / 1000)).toFixed(3),
-                count: keyCount,
-                elapsed: (elapsed / 1000).toFixed(2)
-              }
-            }), 'start'],
-            [t('secret'), 'start'],
-            []
-          ]}
+          header={header}
         >
           {matches.map((match): React.ReactNode => (
             <Match

+ 11 - 9
packages/page-council/src/Motions/index.tsx

@@ -5,7 +5,7 @@
 import { DeriveCollectiveProposals, DeriveCollectiveProposal } from '@polkadot/api-derive/types';
 import { AccountId } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Button, Table } from '@polkadot/react-components';
 import { useMembers } from '@polkadot/react-hooks';
 
@@ -25,6 +25,15 @@ function Proposals ({ className, motions, prime }: Props): React.ReactElement<Pr
   const { t } = useTranslation();
   const { isMember, members } = useMembers();
 
+  const header = useMemo(() => [
+    [t('motions'), 'start', 2],
+    [t('threshold')],
+    [t('voting end')],
+    [t('aye'), 'address'],
+    [t('nay'), 'address'],
+    [undefined, undefined, 2]
+  ], [t]);
+
   return (
     <div className={className}>
       <Button.Group>
@@ -45,14 +54,7 @@ function Proposals ({ className, motions, prime }: Props): React.ReactElement<Pr
       </Button.Group>
       <Table
         empty={motions && t('No council motions')}
-        header={[
-          [t('motions'), 'start', 2],
-          [t('threshold')],
-          [t('voting end')],
-          [t('aye'), 'address'],
-          [t('nay'), 'address'],
-          [undefined, undefined, 2]
-        ]}
+        header={header}
       >
         {motions?.map((motion: DeriveCollectiveProposal): React.ReactNode => (
           <Motion

+ 13 - 9
packages/page-council/src/Overview/Candidates.tsx

@@ -5,7 +5,7 @@
 import { AccountId } from '@polkadot/types/interfaces';
 import { ComponentProps } from './types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -19,14 +19,21 @@ interface Props extends ComponentProps {
 function Candidates ({ allVotes = {}, electionsInfo }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const headerRunners = useMemo(() => [
+    [t('runners up'), 'start', 2],
+    [t('backing')]
+  ], [t]);
+
+  const headerCandidates = useMemo(() => [
+    [t('candidates'), 'start', 2],
+    [t('backing')]
+  ], [t]);
+
   return (
     <>
       <Table
         empty={electionsInfo && t('No runners up found')}
-        header={[
-          [t('runners up'), 'start', 2],
-          [t('backing')]
-        ]}
+        header={headerRunners}
       >
         {electionsInfo?.runnersUp.map(([accountId, balance]): React.ReactNode => (
           <Candidate
@@ -39,10 +46,7 @@ function Candidates ({ allVotes = {}, electionsInfo }: Props): React.ReactElemen
       </Table>
       <Table
         empty={electionsInfo && t('No candidates found')}
-        header={[
-          [t('candidates'), 'start', 2],
-          [t('backing')]
-        ]}
+        header={headerCandidates}
       >
         {electionsInfo?.candidates.map((accountId): React.ReactNode => (
           <Candidate

+ 7 - 5
packages/page-council/src/Overview/Members.tsx

@@ -5,7 +5,7 @@
 import { AccountId } from '@polkadot/types/interfaces';
 import { ComponentProps } from './types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -20,14 +20,16 @@ interface Props extends ComponentProps {
 function Members ({ allVotes = {}, className, electionsInfo, prime }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('members'), 'start', 2],
+    [t('backing')]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={electionsInfo && t('No members found')}
-      header={[
-        [t('members'), 'start', 2],
-        [t('backing')]
-      ]}
+      header={header}
     >
       {electionsInfo?.members.map(([accountId, balance]): React.ReactNode => (
         <Candidate

+ 8 - 6
packages/page-democracy/src/Overview/DispatchQueue.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveDispatch } from '@polkadot/api-derive/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -20,15 +20,17 @@ function DispatchQueue ({ className }: Props): React.ReactElement<Props> | null
   const { api } = useApi();
   const queued = useCall<DeriveDispatch[]>(api.derive.democracy.dispatchQueue, []);
 
+  const header = useMemo(() => [
+    [t('dispatch queue'), 'start', 2],
+    [t('enact')],
+    [undefined, undefined, 2]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={queued && t('Nothing queued for execution')}
-      header={[
-        [t('dispatch queue'), 'start', 2],
-        [t('enact')],
-        [undefined, undefined, 2]
-      ]}
+      header={header}
     >
       {queued?.map((entry): React.ReactNode => (
         <DispatchEntry

+ 9 - 7
packages/page-democracy/src/Overview/Externals.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveProposalExternal } from '@polkadot/api-derive/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -20,16 +20,18 @@ function Externals ({ className }: Props): React.ReactElement<Props> | null {
   const { api } = useApi();
   const external = useCall<DeriveProposalExternal | null>(api.derive.democracy.nextExternal, []);
 
+  const header = useMemo(() => [
+    [t('external'), 'start'],
+    [t('proposer'), 'address'],
+    [t('locked')],
+    []
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={external === null && t('No external proposal')}
-      header={[
-        [t('external'), 'start'],
-        [t('proposer'), 'address'],
-        [t('locked')],
-        []
-      ]}
+      header={header}
     >
       {external && <External value={external} />}
     </Table>

+ 9 - 7
packages/page-democracy/src/Overview/Proposals.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveProposal } from '@polkadot/api-derive/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -20,16 +20,18 @@ function Proposals ({ className }: Props): React.ReactElement<Props> {
   const { api } = useApi();
   const proposals = useCall<DeriveProposal[]>(api.derive.democracy.proposals, []);
 
+  const header = useMemo(() => [
+    [t('proposals'), 'start', 2],
+    [t('proposer'), 'address'],
+    [t('locked')],
+    [undefined, undefined, 3]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={proposals && t('No active proposals')}
-      header={[
-        [t('proposals'), 'start', 2],
-        [t('proposer'), 'address'],
-        [t('locked')],
-        [undefined, undefined, 3]
-      ]}
+      header={header}
     >
       {proposals?.map((proposal): React.ReactNode => (
         <ProposalDisplay

+ 11 - 9
packages/page-democracy/src/Overview/Referendums.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveReferendumExt } from '@polkadot/api-derive/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -20,18 +20,20 @@ function Referendums ({ className }: Props): React.ReactElement<Props> {
   const { api } = useApi();
   const referendums = useCall<DeriveReferendumExt[]>(api.derive.democracy.referendums, []);
 
+  const header = useMemo(() => [
+    [t('referenda'), 'start', 2],
+    [t('remaining')],
+    [t('activate')],
+    [t('aye')],
+    [t('nay')],
+    [undefined, undefined, 3]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={referendums && t('No active referendums')}
-      header={[
-        [t('referenda'), 'start', 2],
-        [t('remaining')],
-        [t('activate')],
-        [t('aye')],
-        [t('nay')],
-        [undefined, undefined, 3]
-      ]}
+      header={header}
     >
       {referendums?.map((referendum): React.ReactNode => (
         <Referendum

+ 6 - 4
packages/page-explorer/src/BlockHeaders.tsx

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { HeaderExtended } from '@polkadot/api-derive';
 import { Table } from '@polkadot/react-components';
 
@@ -16,12 +16,14 @@ interface Props {
 function BlockHeaders ({ headers }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('recent blocks'), 'start', 3]
+  ], [t]);
+
   return (
     <Table
       empty={t('No blocks available')}
-      header={[
-        [t('recent blocks'), 'start', 3]
-      ]}
+      header={header}
     >
       {headers
         .filter((header) => !!header)

+ 8 - 6
packages/page-explorer/src/BlockInfo/Extrinsics.tsx

@@ -4,7 +4,7 @@
 
 import { BlockNumber, EventRecord, Extrinsic } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -21,15 +21,17 @@ interface Props {
 function Extrinsics ({ blockNumber, className, events, label, value }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [label || t('extrinsics'), 'start', 2],
+    [t('events'), 'start', 2],
+    [t('signer'), 'address']
+  ], [label, t]);
+
   return (
     <Table
       className={className}
       empty={t('No pending extrinsics are in the queue')}
-      header={[
-        [label || t('extrinsics'), 'start', 2],
-        [t('events'), 'start', 2],
-        [t('signer'), 'address']
-      ]}
+      header={header}
       isFixed
     >
       {value?.map((extrinsic, index): React.ReactNode =>

+ 4 - 2
packages/page-explorer/src/BlockInfo/Logs.tsx

@@ -5,7 +5,7 @@
 import { DigestItem } from '@polkadot/types/interfaces';
 import { Codec, TypeDef } from '@polkadot/types/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Struct, Tuple, Raw, Vec, getTypeDef } from '@polkadot/types';
 import { Expander, Table } from '@polkadot/react-components';
 import Params from '@polkadot/react-params';
@@ -103,10 +103,12 @@ function Logs (props: Props): React.ReactElement<Props> | null {
   const { value } = props;
   const { t } = useTranslation();
 
+  const header = useMemo(() => [[t('logs'), 'start']], [t]);
+
   return (
     <Table
       empty={t('No logs available')}
-      header={[[t('logs'), 'start']]}
+      header={header}
     >
       {value?.map((log, index) => (
         <tr key={`log:${index}`}>

+ 6 - 4
packages/page-explorer/src/Events.tsx

@@ -4,7 +4,7 @@
 
 import { EventRecord } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 
 import Event from './Event';
@@ -20,12 +20,14 @@ interface Props {
 function Events ({ emptyLabel, eventClassName, events, label }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [label || t('recent events'), 'start']
+  ], [label, t]);
+
   return (
     <Table
       empty={emptyLabel || t('No events available')}
-      header={[
-        [label || t('recent events'), 'start']
-      ]}
+      header={header}
     >
       {events
         .filter(({ event: { method, section } }): boolean => !!method && !!section)

+ 9 - 7
packages/page-explorer/src/NodeInfo/Peers.tsx

@@ -4,7 +4,7 @@
 
 import { PeerInfo } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { formatNumber } from '@polkadot/util';
 
 import { useTranslation } from '../translate';
@@ -18,16 +18,18 @@ interface Props {
 function Peers ({ className, peers }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('connected peers'), 'start'],
+    [t('role'), 'start'],
+    [t('best #'), 'number'],
+    [t('best hash'), 'hash']
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={t('no peers connected')}
-      header={[
-        [t('connected peers'), 'start'],
-        [t('role'), 'start'],
-        [t('best #'), 'number'],
-        [t('best hash'), 'hash']
-      ]}
+      header={header}
     >
       {peers?.sort((a, b): number => b.bestNumber.cmp(a.bestNumber)).map((peer) => (
         <tr key={peer.peerId.toString()}>

+ 8 - 8
packages/page-parachains/src/Overview/Parachains.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveParachain } from '@polkadot/api-derive/types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import styled from 'styled-components';
 import { Table } from '@polkadot/react-components';
 
@@ -18,14 +18,14 @@ interface Props {
 function Parachains ({ parachains }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('parachains'), 'start', 4],
+    [t('swap to id')],
+    [t('scheduling')]
+  ], [t]);
+
   return (
-    <Table
-      header={[
-        [t('parachains'), 'start', 4],
-        [t('swap to id')],
-        [t('scheduling')]
-      ]}
-    >
+    <Table header={header}>
       {parachains.map((parachain): React.ReactNode => (
         <Parachain
           key={parachain.id.toString()}

+ 8 - 6
packages/page-society/src/Overview/Bids.tsx

@@ -4,7 +4,7 @@
 
 import { Bid } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -20,15 +20,17 @@ function Bids ({ className }: Props): React.ReactElement<Props> {
   const { api } = useApi();
   const bids = useCall<Bid[]>(api.query.society.bids, []);
 
+  const header = useMemo(() => [
+    [t('bids'), 'start'],
+    [t('kind')],
+    [t('value')]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={bids && t('No bids')}
-      header={[
-        [t('bids'), 'start'],
-        [t('kind')],
-        [t('value')]
-      ]}
+      header={header}
     >
       {bids?.map((bid): React.ReactNode => (
         <BidRow

+ 9 - 7
packages/page-society/src/Overview/Candidates.tsx

@@ -5,7 +5,7 @@
 import { DeriveSocietyCandidate } from '@polkadot/api-derive/types';
 import { OwnMembers } from '../types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -21,16 +21,18 @@ function Candidates ({ allMembers, className, isMember, ownMembers }: Props): Re
   const { api } = useApi();
   const candidates = useCall<DeriveSocietyCandidate[]>(api.derive.society.candidates, []);
 
+  const header = useMemo(() => [
+    [t('candidates'), 'start'],
+    [t('kind')],
+    [t('value')],
+    [t('votes'), 'start']
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={candidates && t('No candidates')}
-      header={[
-        [t('candidates'), 'start'],
-        [t('kind')],
-        [t('value')],
-        [t('votes'), 'start']
-      ]}
+      header={header}
     >
       {candidates?.map((candidate): React.ReactNode => (
         <Candidate

+ 8 - 6
packages/page-society/src/Overview/Defender.tsx

@@ -6,7 +6,7 @@ import { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
 import { SocietyVote } from '@polkadot/types/interfaces';
 import { OwnMembers, VoteType } from '../types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { AddressSmall, Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -29,6 +29,12 @@ function Defender ({ className, info, isMember, ownMembers }: Props): React.Reac
         .map(({ accountId, vote }): VoteType => [accountId.toString(), vote as SocietyVote])
   });
 
+  const header = useMemo(() => [
+    [t('defender'), 'start'],
+    [t('votes'), 'start'],
+    []
+  ], [t]);
+
   if (!info || !info.hasDefender || !info.defender) {
     return null;
   }
@@ -36,11 +42,7 @@ function Defender ({ className, info, isMember, ownMembers }: Props): React.Reac
   return (
     <Table
       className={className}
-      header={[
-        [t('defender'), 'start'],
-        [t('votes'), 'start'],
-        []
-      ]}
+      header={header}
     >
       <tr>
         <td className='address all'>

+ 7 - 5
packages/page-society/src/Overview/Members.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveSociety, DeriveSocietyMember } from '@polkadot/api-derive/types';
 
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useApi, useCall } from '@polkadot/react-hooks';
 
@@ -28,14 +28,16 @@ function Members ({ className, info }: Props): React.ReactElement<Props> {
     );
   }, [info, members]);
 
+  const header = useMemo(() => [
+    [t('members'), 'start', 3],
+    [t('strikes')]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={info && t('No active members')}
-      header={[
-        [t('members'), 'start', 3],
-        [t('strikes')]
-      ]}
+      header={header}
     >
       {filtered.map((member): React.ReactNode => (
         <Member

+ 21 - 17
packages/page-staking/src/Actions/index.tsx

@@ -5,7 +5,7 @@
 import { ActiveEraInfo, EraIndex } from '@polkadot/types/interfaces';
 
 import BN from 'bn.js';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { Table } from '@polkadot/react-components';
 import { useCall, useApi, useOwnStashes } from '@polkadot/react-hooks';
 import { FormatBalance } from '@polkadot/react-query';
@@ -72,28 +72,32 @@ function Actions ({ allStashes, className, isInElection, next, validators }: Pro
     []
   );
 
+  const header = useMemo(() => [
+    [t('stashes'), 'start'],
+    [t('controller'), 'address'],
+    [t('rewards'), 'number'],
+    [t('bonded'), 'number'],
+    [undefined, undefined, 2]
+  ], [t]);
+
+  const footer = useMemo(() => (
+    <tr>
+      <td colSpan={3} />
+      <td className='number'>
+        {bondedTotal && <FormatBalance value={bondedTotal} />}
+      </td>
+      <td colSpan={2} />
+    </tr>
+  ), [bondedTotal]);
+
   return (
     <div className={className}>
       <NewStake />
       <ElectionBanner isInElection={isInElection} />
       <Table
         empty={t('No funds staked yet. Bond funds to validate or nominate a validator')}
-        footer={
-          <tr>
-            <td colSpan={3} />
-            <td className='number'>
-              {bondedTotal && <FormatBalance value={bondedTotal} />}
-            </td>
-            <td colSpan={2} />
-          </tr>
-        }
-        header={[
-          [t('stashes'), 'start'],
-          [t('controller'), 'address'],
-          [t('rewards'), 'number'],
-          [t('bonded'), 'number'],
-          [undefined, undefined, 2]
-        ]}
+        footer={footer}
+        header={header}
       >
         {foundStashes?.map(([stashId, isOwnStash]): React.ReactNode => (
           <Account

+ 31 - 25
packages/page-staking/src/Overview/CurrentList.tsx

@@ -6,7 +6,7 @@ import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/ty
 import { AccountId, Nominations } from '@polkadot/types/interfaces';
 import { Authors } from '@polkadot/react-query/BlockAuthors';
 
-import React, { useCallback, useContext, useEffect, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { Input, Table } from '@polkadot/react-components';
 import { useApi, useCall, useFavorites } from '@polkadot/react-hooks';
 import { BlockAuthorsContext } from '@polkadot/react-query';
@@ -111,6 +111,33 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv
     );
   }, [nominators]);
 
+  const headerActive = useMemo(() => [
+    [t('intentions'), 'start', 3],
+    [t('nominators'), 'start', 2],
+    [t('commission'), 'number', 1],
+    [undefined, undefined, 3]
+  ], [t]);
+
+  const headerWaiting = useMemo(() => [
+    [t('validators'), 'start', 3],
+    [t('other stake')],
+    [t('own stake')],
+    [t('commission')],
+    [t('points')],
+    [t('last #')],
+    []
+  ], [t]);
+
+  const filter = useMemo(() => (
+    <Input
+      autoFocus
+      isFull
+      label={t('filter by name, address or index')}
+      onChange={setNameFilter}
+      value={nameFilter}
+    />
+  ), [nameFilter, t]);
+
   const _renderRows = useCallback(
     (addresses?: AccountExtend[], isMain?: boolean): React.ReactNode[] =>
       (addresses || []).map(([address, isElected, isFavorite]): React.ReactNode => (
@@ -139,12 +166,7 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv
     ? (
       <Table
         empty={waiting && t('No waiting validators found')}
-        header={[
-          [t('intentions'), 'start', 3],
-          [t('nominators'), 'start', 2],
-          [t('commission'), 'number', 1],
-          [undefined, undefined, 3]
-        ]}
+        header={headerActive}
       >
         {_renderRows(elected, false).concat(..._renderRows(waiting, false))}
       </Table>
@@ -152,24 +174,8 @@ function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOv
     : (
       <Table
         empty={validators && t('No active validators found')}
-        filter={
-          <Input
-            autoFocus
-            isFull
-            label={t('filter by name, address or index')}
-            onChange={setNameFilter}
-            value={nameFilter}
-          />
-        }
-        header={[
-          [t('validators'), 'start', 3],
-          [t('other stake')],
-          [t('own stake')],
-          [t('commission')],
-          [t('points')],
-          [t('last #')],
-          []
-        ]}
+        filter={filter}
+        header={headerWaiting}
       >
         {_renderRows(validators, true)}
       </Table>

+ 30 - 24
packages/page-staking/src/Payouts/index.tsx

@@ -6,7 +6,7 @@ import { DeriveStakerReward } from '@polkadot/api-derive/types';
 import { PayoutStash, PayoutValidator } from './types';
 
 import BN from 'bn.js';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import styled from 'styled-components';
 import { Button, Table } from '@polkadot/react-components';
 import { useApi, useOwnEraRewards } from '@polkadot/react-hooks';
@@ -109,6 +109,32 @@ function Payouts ({ className, isInElection }: Props): React.ReactElement<Props>
     }
   }, [allRewards]);
 
+  const headerStashes = useMemo(() => [
+    [t('payout/stash'), 'start'],
+    [t('eras'), 'start'],
+    [t('available')],
+    [('remaining')],
+    [undefined, undefined, 3]
+  ], [t]);
+
+  const headerValidators = useMemo(() => [
+    [t('payout/validator'), 'start'],
+    [t('eras'), 'start'],
+    [t('available')],
+    [('remaining')],
+    [undefined, undefined, 3]
+  ], [t]);
+
+  const footer = useMemo(() => (
+    <tr>
+      <td colSpan={2} />
+      <td className='number'>
+        {stashTotal && <FormatBalance value={stashTotal} />}
+      </td>
+      <td colSpan={4} />
+    </tr>
+  ), [stashTotal]);
+
   return (
     <div className={className}>
       {api.tx.staking.payoutStakers && (
@@ -123,22 +149,8 @@ function Payouts ({ className, isInElection }: Props): React.ReactElement<Props>
       <Table
         empty={stashes && t('No pending payouts for your stashes')}
         emptySpinner={t('Retrieving info for all applicable eras, this will take some time')}
-        footer={
-          <tr>
-            <td colSpan={2} />
-            <td className='number'>
-              {stashTotal && <FormatBalance value={stashTotal} />}
-            </td>
-            <td colSpan={4} />
-          </tr>
-        }
-        header={[
-          [t('payout/stash'), 'start'],
-          [t('eras'), 'start'],
-          [t('available')],
-          [('remaining')],
-          [undefined, undefined, 3]
-        ]}
+        footer={footer}
+        header={headerStashes}
         isFixed
       >
         {stashes?.map((payout): React.ReactNode => (
@@ -153,13 +165,7 @@ function Payouts ({ className, isInElection }: Props): React.ReactElement<Props>
       {api.tx.staking.payoutStakers && (
         <Table
           empty={validators && t('No pending era payouts from validators')}
-          header={[
-            [t('payout/validator'), 'start'],
-            [t('eras'), 'start'],
-            [t('available')],
-            [('remaining')],
-            [undefined, undefined, 3]
-          ]}
+          header={headerValidators}
           isFixed
         >
           {validators?.map((payout): React.ReactNode => (

+ 24 - 20
packages/page-staking/src/Targets/index.tsx

@@ -210,6 +210,28 @@ function Targets ({ className }: Props): React.ReactElement<Props> {
     }
   }, [allAccounts, amount, electedInfo, favorites, lastReward, sortBy, sortFromMax]);
 
+  const header = useMemo(() => [
+    [t('validators'), 'start', 3],
+    ...['rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'].map((header) => [
+      <>{labels[header]}<Icon name={sortBy === header ? (sortFromMax ? 'chevron down' : 'chevron up') : 'minus'} /></>,
+      `isClickable ${sortBy === header && 'ui--highlight--border'} number`,
+      1,
+      (): void => _sort(header as 'rankComm')
+    ]),
+    []
+  ], [_sort, labels, sortBy, sortFromMax, t]);
+
+  const filter = useMemo(() => (
+    <InputBalance
+      className='balanceInput'
+      help={t('The amount that will be used on a per-validator basis to calculate rewards for that validator.')}
+      isFull
+      label={t('amount to use for estimation')}
+      onChange={setAmount}
+      value={_amount}
+    />
+  ), [_amount, t]);
+
   return (
     <div className={className}>
       <Summary
@@ -220,26 +242,8 @@ function Targets ({ className }: Props): React.ReactElement<Props> {
       />
       <Table
         empty={sorted && t('No active validators to check for rewards available')}
-        filter={
-          <InputBalance
-            className='balanceInput'
-            help={t('The amount that will be used on a per-validator basis to calculate rewards for that validator.')}
-            isFull
-            label={t('amount to use for estimation')}
-            onChange={setAmount}
-            value={_amount}
-          />
-        }
-        header={[
-          [t('validators'), 'start', 3],
-          ...['rankComm', 'rankBondTotal', 'rankBondOwn', 'rankBondOther', 'rankOverall'].map((header) => [
-            <>{labels[header]}<Icon name={sortBy === header ? (sortFromMax ? 'chevron down' : 'chevron up') : 'minus'} /></>,
-            `isClickable ${sortBy === header && 'ui--highlight--border'} number`,
-            1,
-            (): void => _sort(header as 'rankComm')
-          ]),
-          []
-        ]}
+        filter={filter}
+        header={header}
       >
         {sorted?.map((info): React.ReactNode =>
           <Validator

+ 6 - 4
packages/page-tech-comm/src/Overview/Members.tsx

@@ -4,7 +4,7 @@
 
 import { AccountId } from '@polkadot/types/interfaces';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { AddressSmall, Table, Tag } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -18,13 +18,15 @@ interface Props {
 function Members ({ className, members, prime }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('members'), 'start', 3]
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={members && t('No members found')}
-      header={[
-        [t('members'), 'start', 3]
-      ]}
+      header={header}
     >
       {members?.map((accountId): React.ReactNode => (
         <tr key={accountId.toString()}>

+ 10 - 8
packages/page-tech-comm/src/Proposals/index.tsx

@@ -5,7 +5,7 @@
 import { Hash } from '@polkadot/types/interfaces';
 import { ComponentProps as Props } from '../types';
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Button, Table } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -15,6 +15,14 @@ import Propose from './Propose';
 function Proposals ({ className, isMember, members, prime, proposals }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('proposals'), 'start', 2],
+    [t('threshold')],
+    [t('aye'), 'address'],
+    [t('nay'), 'address'],
+    []
+  ], [t]);
+
   return (
     <div className={className}>
       <Button.Group>
@@ -25,13 +33,7 @@ function Proposals ({ className, isMember, members, prime, proposals }: Props):
       </Button.Group>
       <Table
         empty={proposals && t('No committee proposals')}
-        header={[
-          [t('proposals'), 'start', 2],
-          [t('threshold')],
-          [t('aye'), 'address'],
-          [t('nay'), 'address'],
-          []
-        ]}
+        header={header}
       >
         {proposals?.map((hash: Hash): React.ReactNode => (
           <Proposal

+ 10 - 8
packages/page-treasury/src/Overview/Proposals.tsx

@@ -4,7 +4,7 @@
 
 import { DeriveTreasuryProposal } from '@polkadot/api-derive/types';
 
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
 import { useHistory } from 'react-router-dom';
 import { Table } from '@polkadot/react-components';
 
@@ -29,17 +29,19 @@ function ProposalsBase ({ className, isApprovals, isMember, proposals }: Props):
     [history]
   );
 
+  const header = useMemo(() => [
+    [isApprovals ? t('Approved') : t('Proposals'), 'start', 2],
+    [t('beneficiary'), 'address'],
+    [t('payment')],
+    [t('bond')],
+    [undefined, undefined, 2]
+  ], [isApprovals, t]);
+
   return (
     <Table
       className={className}
       empty={proposals && (isApprovals ? t('No approved proposals') : t('No pending proposals'))}
-      header={[
-        [isApprovals ? t('Approved') : t('Proposals'), 'start', 2],
-        [t('beneficiary'), 'address'],
-        [t('payment')],
-        [t('bond')],
-        [undefined, undefined, 2]
-      ]}
+      header={header}
     >
       {proposals?.map((proposal): React.ReactNode => (
         <Proposal

+ 11 - 9
packages/page-treasury/src/Overview/Tips.tsx

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import React from 'react';
+import React, { useMemo } from 'react';
 import { Table } from '@polkadot/react-components';
 
 import { useTranslation } from '../translate';
@@ -18,18 +18,20 @@ interface Props {
 function Tips ({ className, hashes, isMember, members }: Props): React.ReactElement<Props> {
   const { t } = useTranslation();
 
+  const header = useMemo(() => [
+    [t('tips'), 'start'],
+    [t('finder'), 'address'],
+    [t('fee')],
+    [t('reason'), 'start'],
+    [],
+    []
+  ], [t]);
+
   return (
     <Table
       className={className}
       empty={hashes && t('No open tips')}
-      header={[
-        [t('tips'), 'start'],
-        [t('finder'), 'address'],
-        [t('fee')],
-        [t('reason'), 'start'],
-        [],
-        []
-      ]}
+      header={header}
     >
       {hashes?.map((hash): React.ReactNode => (
         <Tip