perf: improve token details query performance (#4808)
* updated hook * fixed build error
This commit is contained in:
parent
9037930e56
commit
66fad96e61
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user