Commit 66fad96e authored by cartcrom's avatar cartcrom Committed by GitHub

perf: improve token details query performance (#4808)

* updated hook
* fixed build error
parent 9037930e
...@@ -6,15 +6,16 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon' ...@@ -6,15 +6,16 @@ import { VerifiedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' 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 { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs' import useCurrencyLogoURIs, { getTokenLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { textFadeIn } from 'theme/animations' import { textFadeIn } from 'theme/animations'
import { isAddress } from 'utils' import { isAddress } from 'utils'
import { useIsFavorited, useToggleFavorite } from '../state' import { filterTimeAtom, useIsFavorited, useToggleFavorite } from '../state'
import { ClickFavorited, FavoriteIcon, L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow' import { ClickFavorited, FavoriteIcon, L2NetworkLogo, LogoContainer } from '../TokenTable/TokenRow'
import PriceChart from './PriceChart' import PriceChart from './PriceChart'
import ShareButton from './ShareButton' import ShareButton from './ShareButton'
...@@ -71,17 +72,18 @@ export function useTokenLogoURI( ...@@ -71,17 +72,18 @@ export function useTokenLogoURI(
export default function ChartSection({ export default function ChartSection({
token, token,
nativeCurrency, nativeCurrency,
prices,
}: { }: {
token: NonNullable<SingleTokenData> token: NonNullable<SingleTokenData>
nativeCurrency?: Token | NativeCurrency nativeCurrency?: Token | NativeCurrency
prices: PriceDurations
}) { }) {
const isFavorited = useIsFavorited(token.address) const isFavorited = useIsFavorited(token.address)
const toggleFavorite = useToggleFavorite(token.address) const toggleFavorite = useToggleFavorite(token.address)
const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain] const chainId = CHAIN_NAME_TO_CHAIN_ID[token.chain]
const L2Icon = getChainInfo(chainId).circleLogoUrl const L2Icon = getChainInfo(chainId).circleLogoUrl
const warning = checkWarning(token.address ?? '') const warning = checkWarning(token.address ?? '')
const timePeriod = useAtomValue(filterTimeAtom)
const { prices } = useTokenPricesCached(token)
const logoSrc = useTokenLogoURI(token, nativeCurrency) const logoSrc = useTokenLogoURI(token, nativeCurrency)
...@@ -110,7 +112,7 @@ export default function ChartSection({ ...@@ -110,7 +112,7 @@ export default function ChartSection({
</TokenInfoContainer> </TokenInfoContainer>
<ChartContainer> <ChartContainer>
<ParentSize> <ParentSize>
{({ width, height }) => prices && <PriceChart prices={prices} width={width} height={height} />} {({ width, height }) => prices && <PriceChart prices={prices[timePeriod]} width={width} height={height} />}
</ParentSize> </ParentSize>
</ChartContainer> </ChartContainer>
</ChartHeader> </ChartHeader>
......
import graphql from 'babel-plugin-relay/macro' import graphql from 'babel-plugin-relay/macro'
import { filterTimeAtom } from 'components/Tokens/state' import { useMemo, useState } from 'react'
import { useAtomValue } from 'jotai/utils' import { fetchQuery, useLazyLoadQuery } from 'react-relay'
import { useCallback, useEffect, useState } from 'react'
import { fetchQuery, useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql' import { Chain, TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
import { TokenPrices$data, TokenPrices$key } from './__generated__/TokenPrices.graphql' import { ContractInput, HistoryDuration, TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql'
import { TokenQuery, TokenQuery$data } from './__generated__/TokenQuery.graphql' import environment from './RelayEnvironment'
import { TimePeriod, toHistoryDuration } from './util' 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: The difference between Token and TokenProject:
Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984) Token: an on-chain entity referring to a contract (e.g. uni token on ethereum 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984)
...@@ -81,13 +61,79 @@ const tokenQuery = graphql` ...@@ -81,13 +61,79 @@ const tokenQuery = graphql`
} }
` `
export function useTokenQuery(address: string, chain: Chain, timePeriod: TimePeriod) { export type PricePoint = { value: number; timestamp: number }
const data = useLazyLoadQuery<TokenQuery>(tokenQuery, { export function filterPrices(prices: NonNullable<NonNullable<SingleTokenData>['market']>['priceHistory'] | undefined) {
contract: { address: address.toLowerCase(), chain }, return prices?.filter((p): p is PricePoint => Boolean(p && p.value))
duration: toHistoryDuration(timePeriod), }
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,
})
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]
return data useMemo(
() =>
setPrices((current) => {
current[originalTimePeriod] = filterPrices(token?.market?.priceHistory)
return current
}),
[token, originalTimePeriod]
)
return [token, prices]
} }
const tokenPriceQuery = graphql` const tokenPriceQuery = graphql`
...@@ -125,60 +171,3 @@ 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() { ...@@ -110,7 +110,7 @@ export default function TokenDetails() {
const timePeriod = useAtomValue(filterTimeAtom) const timePeriod = useAtomValue(filterTimeAtom)
const currentChainName = validateUrlChainParam(chainName) const currentChainName = validateUrlChainParam(chainName)
const token = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod).tokens?.[0] const [token, prices] = useTokenQuery(tokenAddress ?? '', currentChainName, timePeriod)
const navigate = useNavigate() const navigate = useNavigate()
const switchChains = useCallback( const switchChains = useCallback(
...@@ -212,7 +212,7 @@ export default function TokenDetails() { ...@@ -212,7 +212,7 @@ export default function TokenDetails() {
<BreadcrumbNavLink to={`/tokens/${chainName}`}> <BreadcrumbNavLink to={`/tokens/${chainName}`}>
<ArrowLeft size={14} /> Tokens <ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink> </BreadcrumbNavLink>
<ChartSection token={token} nativeCurrency={nativeCurrency} /> <ChartSection token={token} prices={prices} nativeCurrency={nativeCurrency} />
<StatsSection <StatsSection
TVL={token.market?.totalValueLocked?.value} TVL={token.market?.totalValueLocked?.value}
volume24H={token.market?.volume24H?.value} volume24H={token.market?.volume24H?.value}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment