diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index 0af0ae8..3a5cec6 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; @@ -61,6 +61,24 @@ const balanceFormat = new Intl.NumberFormat(undefined, { useGrouping: true, }); +// Aggregated $USD values of all child BalanceListItems child components. +// +// Values: +// * undefined => loading. +// * null => no market exists. +// * float => done. +// +// For a given set of publicKeys, we know all the USD values have been loaded when +// all of their values in this object are not `undefined`. +const usdValues = {}; + +function fairsIsLoaded(publicKeys) { + return ( + publicKeys.filter((pk) => usdValues[pk.toString()] !== undefined).length === + publicKeys.length + ); +} + export default function BalancesList() { const wallet = useWallet(); const [publicKeys, loaded] = useWalletPublicKeys(); @@ -69,22 +87,64 @@ export default function BalancesList() { false, ); const [showMergeAccounts, setShowMergeAccounts] = useState(false); - const [usdValues, setUsdValues] = useState({}); const { accounts, setAccountName } = useWalletSelector(); + // Dummy var to force rerenders on demand. + const [, setForceUpdate] = useState(false); const selectedAccount = accounts.find((a) => a.isSelected); - let totalUsdValue = publicKeys - .filter((pk) => usdValues[pk.toString()] !== undefined) + const totalUsdValue = publicKeys + .filter((pk) => usdValues[pk.toString()]) .map((pk) => usdValues[pk.toString()]) .reduce((a, b) => a + b, 0.0); + // Memoized callback and component for the `BalanceListItems`. + // + // The `BalancesList` fetches data, e.g., fairs for tokens using React hooks + // in each of the child `BalanceListItem` components. However, we want the + // parent component, to aggregate all of this data together, for example, + // to show the cumulative USD amount in the wallet. + // + // To achieve this, we need to pass a callback from the parent to the chlid, + // so that the parent can collect the results of all the async network requests. + // However, this can cause a render loop, since invoking the callback can cause + // the parent to rerender, which causese the child to rerender, which causes + // the callback to be invoked. + // + // To solve this, we memoize all the `BalanceListItem` children components. + const setUsdValuesCallback = useCallback( + (publicKey, usdValue) => { + if (usdValues[publicKey.toString()] !== usdValue) { + usdValues[publicKey.toString()] = usdValue; + if (fairsIsLoaded(publicKeys)) { + setForceUpdate((forceUpdate) => !forceUpdate); + } + } + }, + [publicKeys], + ); + const balanceListItemsMemo = useMemo(() => { + return publicKeys.map((pk) => { + return React.memo((props) => { + return ( + + ); + }); + }); + }, [publicKeys, setUsdValuesCallback]); + return ( - {selectedAccount && selectedAccount.name} Balances ($ - {totalUsdValue.toFixed(2)}) + {selectedAccount && selectedAccount.name} Balances{' '} + {loaded && fairsIsLoaded(publicKeys) && ( + <>(${totalUsdValue.toFixed(2)}) + )} {selectedAccount && selectedAccount.name !== 'Main account' && @@ -121,18 +181,8 @@ export default function BalancesList() { - {publicKeys.map((publicKey) => ( - { - if (usdValues[publicKey.toString()] !== usdValue) { - const _usdValues = { ...usdValues }; - _usdValues[publicKey.toString()] = usdValue; - setUsdValues(_usdValues); - } - }} - /> + {balanceListItemsMemo.map((Memoized) => ( + ))} {loaded ? null : } @@ -249,11 +299,18 @@ export function BalanceListItem({ publicKey, expandable, setUsdValue }) { ); - const usdValue = price - ? ((amount / Math.pow(10, decimals)) * price).toFixed(2) - : undefined; - if (usdValue && setUsdValue) { - setUsdValue(parseFloat(usdValue)); + + console.log('balance', publicKey.toString(), amount, balanceInfo); + console.log('price', price); + const usdValue = + price === undefined // Not yet loaded. + ? undefined + : price === null // Loaded and empty. + ? null + : ((amount / Math.pow(10, decimals)) * price).toFixed(2); // Loaded. + if (setUsdValue && usdValue !== undefined) { + console.log('sd calc', usdValue, amount, price, decimals); + setUsdValue(publicKey, usdValue === null ? null : parseFloat(usdValue)); } return ( diff --git a/src/utils/markets.ts b/src/utils/markets.ts index 5d1e8af..8b65fae 100644 --- a/src/utils/markets.ts +++ b/src/utils/markets.ts @@ -53,9 +53,9 @@ class PriceStore { if (resp.data.asks.length === 0 && resp.data.bids.length === 0) { resolve(undefined); } else if (resp.data.asks.length === 0) { - resolve(resp.data.bids[0]); + resolve(resp.data.bids[0].price); } else if (resp.data.bids.length === 0) { - resolve(resp.data.asks[0]); + resolve(resp.data.asks[0].price); } else { const mid = (resp.data.asks[0].price + resp.data.bids[0].price) / 2.0;