perf: improve token details query performance (#4808)

* updated hook
* fixed build error
This commit is contained in:
cartcrom 2022-10-05 15:50:08 -04:00 committed by GitHub
parent 9037930e56
commit 66fad96e61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 93 deletions

@ -6,15 +6,16 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { SingleTokenData, useTokenPricesCached } from 'graphql/data/Token'
import { PriceDurations, SingleTokenData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
import styled from 'styled-components/macro'
import { textFadeIn } from 'theme/animations'
import { isAddress } from 'utils'
import { useIsFavorited, useToggleFavorite } from '../state'
import { filterTimeAtom, useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon, L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
import PriceChart from './PriceChart'
import ShareButton from './ShareButton'
@ -71,17 +72,18 @@ export function useTokenLogoURI(
export default function ChartSection({
token,
nativeCurrency,
prices,
}: {
token: NonNullable<SingleTokenData>
nativeCurrency?: Token | NativeCurrency
prices: PriceDurations
}) {
const isFavorited = useIsFavorited(token.address)
const toggleFavorite = useToggleFavorite(token.address)
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId).circleLogoUrl
const warning = checkWarning(token.address ?? '')
const { prices } = useTokenPricesCached(token)
const timePeriod = useAtomValue(filterTimeAtom)
const logoSrc = useTokenLogoURI(token, nativeCurrency)
@ -110,7 +112,7 @@ export default function ChartSection({
</TokenInfoContainer>
<ChartContainer>
<ParentSize>
{({ width, height }) => prices && <PriceChart prices={prices} width={width} height={height} />}
{({ width, height }) => prices && <PriceChart prices={prices[timePeriod]} width={width} height={height} />}
</ParentSize>
</ChartContainer>
</ChartHeader>

@ -1,32 +1,12 @@
import graphql from 'babel-plugin-relay/macro'
import { filterTimeAtom } from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useState } from 'react'
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery } from 'react-relay'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql'
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import environment from './RelayEnvironment'
import { TimePeriod, toHistoryDuration } from './util'
export type PricePoint = { value: number; timestamp: number }
export const projectMetaDataFragment = graphql`
fragment Token_TokenProject_Metadata on TokenProject {
description
homepageUrl
twitterName
name
}
`
const tokenPricesFragment = graphql`
fragment TokenPrices on TokenProjectMarket {
priceHistory(duration: $duration) {
timestamp
value
}
}
`
/*
The difference between Token and TokenProject:
Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984)
@ -81,13 +61,79 @@ const tokenQuery = graphql`
}
`
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) {
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract: { address: address.toLowerCase(), chain },
duration: toHistoryDuration(timePeriod),
export type PricePoint = { value: number; timestamp: number }
export function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) {
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
}
export type PriceDurations = Record<TimePeriod, PricePoint[] | undefined>
function fetchAllPriceDurations(contract: ContractInput, originalDuration: HistoryDuration) {
return fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, {
contract,
skip1H: originalDuration === 'HOUR',
skip1D: originalDuration === 'DAY',
skip1W: originalDuration === 'WEEK',
skip1M: originalDuration === 'MONTH',
skip1Y: originalDuration === 'YEAR',
})
}
export type SingleTokenData = NonNullable<TokenQuery$data['tokens']>[number]
export function useTokenQuery(
address: string,
chain: Chain,
timePeriod: TimePeriod
): [SingleTokenData | undefined, PriceDurations] {
const [prices, setPrices] = useState<PriceDurations>({
[TimePeriod.HOUR]: undefined,
[TimePeriod.DAY]: undefined,
[TimePeriod.WEEK]: undefined,
[TimePeriod.MONTH]: undefined,
[TimePeriod.YEAR]: undefined,
})
return data
const contract = useMemo(() => {
return { address: address.toLowerCase(), chain }
}, [address, chain])
// eslint-disable-next-line react-hooks/exhaustive-deps
const originalTimePeriod = useMemo(() => timePeriod, [contract])
const updatePrices = (response: TokenPriceQuery['response']) => {
const priceData = response.tokens?.[0]?.market
if (priceData) {
setPrices((current) => {
return {
[TimePeriod.HOUR]: filterPrices(priceData.priceHistory1H) ?? current[TimePeriod.HOUR],
[TimePeriod.DAY]: filterPrices(priceData.priceHistory1D) ?? current[TimePeriod.DAY],
[TimePeriod.WEEK]: filterPrices(priceData.priceHistory1W) ?? current[TimePeriod.WEEK],
[TimePeriod.MONTH]: filterPrices(priceData.priceHistory1M) ?? current[TimePeriod.MONTH],
[TimePeriod.YEAR]: filterPrices(priceData.priceHistory1Y) ?? current[TimePeriod.YEAR],
}
})
}
}
// Fetch prices & token info in tandem so we can render faster
useMemo(
() => fetchAllPriceDurations(contract, toHistoryDuration(originalTimePeriod)).subscribe({ next: updatePrices }),
[contract, originalTimePeriod]
)
const token = useLazyLoadQuery<TokenQuery>(tokenQuery, {
contract,
duration: toHistoryDuration(originalTimePeriod),
}).tokens?.[0]
useMemo(
() =>
setPrices((current) => {
current[originalTimePeriod] = filterPrices(token?.market?.priceHistory)
return current
}),
[token, originalTimePeriod]
)
return [token, prices]
}
const tokenPriceQuery = graphql`
@ -125,60 +171,3 @@ const tokenPriceQuery = graphql`
}
}
`
export function filterPrices(prices: TokenPrices$data['priceHistory'] | undefined) {
return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
}
export function useTokenPricesFromFragment(key: TokenPrices$key | null | undefined) {
const fetchedTokenPrices = useFragment(tokenPricesFragment, key ?? null)?.priceHistory
return filterPrices(fetchedTokenPrices)
}
export function useTokenPricesCached(token: SingleTokenData) {
// Attempt to use token prices already provided by TokenDetails / TopToken queries
const environment = useRelayEnvironment()
const timePeriod = useAtomValue(filterTimeAtom)
const [priceMap, setPriceMap] = useState<Map<TimePeriod, PricePoint[] | undefined>>(new Map())
const updatePrices = useCallback(
(key: TimePeriod, data?: PricePoint[]) => {
setPriceMap(new Map(priceMap.set(key, data)))
},
[priceMap]
)
// Fetch the other timePeriods after first render
useEffect(() => {
const fetchedTokenPrices = token?.market?.priceHistory
updatePrices(timePeriod, filterPrices(fetchedTokenPrices))
// Fetch all time periods except the one already populated
if (token?.chain && token?.address) {
fetchQuery<TokenPriceQuery>(environment, tokenPriceQuery, {
contract: { address: token.address, chain: token.chain },
skip1H: timePeriod === TimePeriod.HOUR && !!fetchedTokenPrices,
skip1D: timePeriod === TimePeriod.DAY && !!fetchedTokenPrices,
skip1W: timePeriod === TimePeriod.WEEK && !!fetchedTokenPrices,
skip1M: timePeriod === TimePeriod.MONTH && !!fetchedTokenPrices,
skip1Y: timePeriod === TimePeriod.YEAR && !!fetchedTokenPrices,
}).subscribe({
next: (data) => {
const market = data.tokens?.[0]?.market
if (market) {
market.priceHistory1H && updatePrices(TimePeriod.HOUR, filterPrices(market.priceHistory1H))
market.priceHistory1D && updatePrices(TimePeriod.DAY, filterPrices(market.priceHistory1D))
market.priceHistory1W && updatePrices(TimePeriod.WEEK, filterPrices(market.priceHistory1W))
market.priceHistory1M && updatePrices(TimePeriod.MONTH, filterPrices(market.priceHistory1M))
market.priceHistory1Y && updatePrices(TimePeriod.YEAR, filterPrices(market.priceHistory1Y))
}
},
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token?.chain, token?.address])
return { prices: priceMap.get(timePeriod) }
}
export type SingleTokenData = NonNullable<TokenQuery$data['tokens']>[number]

@ -110,7 +110,7 @@ export default function TokenDetails() {
const timePeriod = useAtomValue(filterTimeAtom)
const currentChainName = validateUrlChainParam(chainName)
const token = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod).tokens?.[0]
const [token, prices] = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod)
const navigate = useNavigate()
const switchChains = useCallback(
@ -212,7 +212,7 @@ export default function TokenDetails() {
<BreadcrumbNavLink to={`/tokens/${chainName}`}>
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<ChartSection token={token} nativeCurrency={nativeCurrency} />
<ChartSection token={token} prices={prices} nativeCurrency={nativeCurrency} />
<StatsSection
TVL={token.market?.totalValueLocked?.value}
volume24H={token.market?.volume24H?.value}