feat: lazy load sparklines (#4827)

* testing removing sparklines

* fixed build

* filter working

* added lazy loading of sparklines

* fixed bugs

* removed comments

* add back memo

Co-authored-by: Connor McEwen <connor.mcewen@gmail.com>
This commit is contained in:
cartcrom 2022-10-10 16:51:45 -04:00 committed by GitHub
parent 351f66a83e
commit d8677d8a6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 349 deletions

@ -1,14 +1,20 @@
import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
import { curveCardinal, scaleLinear } from 'd3' import { curveCardinal, scaleLinear } from 'd3'
import { filterPrices } from 'graphql/data/Token' import { PricePoint } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import React from 'react' import { memo } from 'react'
import { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { DATA_EMPTY, getPriceBounds } from '../Tokens/TokenDetails/PriceChart' import { getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
import LineChart from './LineChart' import LineChart from './LineChart'
type PricePoint = { value: number; timestamp: number } const LoadingContainer = styled.div`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`
interface SparklineChartProps { interface SparklineChartProps {
width: number width: number
@ -16,15 +22,32 @@ interface SparklineChartProps {
tokenData: TopToken tokenData: TopToken
pricePercentChange: number | undefined | null pricePercentChange: number | undefined | null
timePeriod: TimePeriod timePeriod: TimePeriod
sparklineMap: SparklineMap
} }
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) { function _SparklineChart({
width,
height,
tokenData,
pricePercentChange,
timePeriod,
sparklineMap,
}: SparklineChartProps) {
const theme = useTheme() const theme = useTheme()
// for sparkline // for sparkline
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? [] const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
const hasData = pricePoints.length !== 0
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY // Don't display if there's one or less pricepoints
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY if (!pricePoints || pricePoints.length <= 1) {
return (
<LoadingContainer>
<SparkLineLoadingBubble />
</LoadingContainer>
)
}
const startingPrice = pricePoints[0]
const endingPrice = pricePoints[pricePoints.length - 1]
const widthScale = scaleLinear() const widthScale = scaleLinear()
.domain( .domain(
// the range of possible input values // the range of possible input values
@ -52,4 +75,4 @@ function SparklineChart({ width, height, tokenData, pricePercentChange, timePeri
) )
} }
export default React.memo(SparklineChart) export default memo(_SparklineChart)

@ -1,11 +1,11 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/animations' import { textFadeIn } from 'theme/animations'
import { formatDollar } from 'utils/formatDollarAmt' import { formatDollar } from 'utils/formatDollarAmt'
import { TokenSortMethod } from '../state'
import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow' import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow'
import InfoTip from './InfoTip' import InfoTip from './InfoTip'

@ -6,7 +6,7 @@ import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ForwardedRef, forwardRef } from 'react' import { ForwardedRef, forwardRef } from 'react'
@ -30,6 +30,7 @@ import {
filterTimeAtom, filterTimeAtom,
sortAscendingAtom, sortAscendingAtom,
sortMethodAtom, sortMethodAtom,
TokenSortMethod,
useIsFavorited, useIsFavorited,
useSetSortMethod, useSetSortMethod,
useToggleFavorite, useToggleFavorite,
@ -310,7 +311,7 @@ const IconLoadingBubble = styled(LoadingBubble)`
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
` `
const SparkLineLoadingBubble = styled(LongLoadingBubble)` export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
height: 4px; height: 4px;
` `
@ -395,7 +396,7 @@ export function TokenRow({
marketCap: ReactNode marketCap: ReactNode
price: ReactNode price: ReactNode
percentChange: ReactNode percentChange: ReactNode
sparkLine: ReactNode sparkLine?: ReactNode
tokenInfo: ReactNode tokenInfo: ReactNode
volume: ReactNode volume: ReactNode
last?: boolean last?: boolean
@ -466,6 +467,7 @@ interface LoadedRowProps {
tokenListIndex: number tokenListIndex: number
tokenListLength: number tokenListLength: number
token: NonNullable<TopToken> token: NonNullable<TopToken>
sparklineMap: SparklineMap
} }
/* Loaded State: row component with token information */ /* Loaded State: row component with token information */
@ -477,6 +479,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const isFavorited = useIsFavorited(tokenAddress) const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress) const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum' const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase() const filterNetwork = lowercaseChainName.toUpperCase()
@ -515,7 +518,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
<FavoriteIcon isFavorited={isFavorited} /> <FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited> </ClickFavorited>
} }
listNumber={tokenListIndex + 1} listNumber={sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1}
tokenInfo={ tokenInfo={
<ClickableName> <ClickableName>
<LogoContainer> <LogoContainer>
@ -558,15 +561,18 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
sparkLine={ sparkLine={
<SparkLine> <SparkLine>
<ParentSize> <ParentSize>
{({ width, height }) => ( {({ width, height }) =>
<SparklineChart props.sparklineMap && (
width={width} <SparklineChart
height={height} width={width}
tokenData={token} height={height}
pricePercentChange={token.market?.pricePercentChange?.value} tokenData={token}
timePeriod={timePeriod} pricePercentChange={token.market?.pricePercentChange?.value}
/> timePeriod={timePeriod}
)} sparklineMap={props.sparklineMap}
/>
)
}
</ParentSize> </ParentSize>
</SparkLine> </SparkLine>
} }

@ -3,7 +3,7 @@ import { showFavoritesAtom } from 'components/Tokens/state'
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens' import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
import { validateUrlChainParam } from 'graphql/data/util' import { validateUrlChainParam } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode, useCallback, useRef } from 'react' import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
@ -11,8 +11,6 @@ import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow' import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow'
const LOADING_ROWS_COUNT = 3
const GridContainer = styled.div` const GridContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -58,13 +56,19 @@ function NoTokensState({ message }: { message: ReactNode }) {
) )
} }
const LoadingRows = (rowCount?: number) => const LoadingRowsWrapper = styled.div`
Array(rowCount ?? PAGE_SIZE) margin-top: 8px;
.fill(null) `
.map((_, index) => {
return <LoadingRow key={index} /> const LoadingRows = (rowCount?: number) => (
}) <LoadingRowsWrapper>
const LoadingMoreRows = LoadingRows(LOADING_ROWS_COUNT) {Array(rowCount ?? PAGE_SIZE)
.fill(null)
.map((_, index) => {
return <LoadingRow key={index} />
})}
</LoadingRowsWrapper>
)
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) { export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
return ( return (
@ -75,73 +79,51 @@ export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
) )
} }
export default function TokenTable() { export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom) const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s // TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { error, loading, tokens, hasMore, loadMoreTokens, loadingRowCount } = useTopTokens(chainName) const { tokens, sparklines } = useTopTokens(chainName)
const showMoreLoadingRows = Boolean(loading && hasMore) setRowCount(tokens?.length ?? PAGE_SIZE)
const observer = useRef<IntersectionObserver>()
const lastTokenRef = useCallback(
(node: HTMLDivElement) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMoreTokens()
}
})
if (node) observer.current.observe(node)
},
[loading, hasMore, loadMoreTokens]
)
/* loading and error state */ /* loading and error state */
if (loading && (!tokens || tokens?.length === 0)) { if (!tokens) {
return <LoadingTokenTable rowCount={loadingRowCount} /> return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occurred loading tokens. Please try again.</Trans>
</>
}
/>
)
} else if (tokens?.length === 0) {
return showFavorites ? (
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
) : (
<NoTokensState message={<Trans>No tokens found</Trans>} />
)
} else { } else {
if (error || !tokens) { return (
return ( <GridContainer>
<NoTokensState <HeaderRow />
message={ <TokenDataContainer>
<> {tokens.map(
<AlertTriangle size={16} /> (token, index) =>
<Trans>An error occurred loading tokens. Please try again.</Trans> token && (
</> <LoadedRow
} key={token?.address}
/> tokenListIndex={index}
) tokenListLength={tokens.length}
} else if (tokens?.length === 0) { token={token}
return showFavorites ? ( sparklineMap={sparklines}
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} /> />
) : ( )
<NoTokensState message={<Trans>No tokens found</Trans>} /> )}
) </TokenDataContainer>
} else { </GridContainer>
return ( )
<>
<GridContainer>
<HeaderRow />
<TokenDataContainer>
{tokens.map(
(token, index) =>
token && (
<LoadedRow
key={token?.address}
tokenListIndex={index}
tokenListLength={tokens.length}
token={token}
ref={index + 1 === tokens.length ? lastTokenRef : undefined}
/>
)
)}
{showMoreLoadingRows && LoadingMoreRows}
</TokenDataContainer>
</GridContainer>
</>
)
}
} }
} }

@ -1,9 +1,15 @@
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils' import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
export enum TokenSortMethod {
PRICE = 'Price',
PERCENT_CHANGE = 'Change',
TOTAL_VALUE_LOCKED = 'TVL',
VOLUME = 'Volume',
}
export const favoritesAtom = atomWithStorage<string[]>('favorites', []) export const favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false) export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('') export const filterStringAtom = atomWithReset<string>('')

@ -6,18 +6,15 @@ import {
showFavoritesAtom, showFavoritesAtom,
sortAscendingAtom, sortAscendingAtom,
sortMethodAtom, sortMethodAtom,
TokenSortMethod,
} from 'components/Tokens/state' } from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { fetchQuery, useRelayEnvironment } from 'react-relay' import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
Chain, import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
ContractInput, import { filterPrices, PricePoint } from './Token'
HistoryDuration,
TopTokens_TokensQuery,
} from './__generated__/TopTokens_TokensQuery.graphql'
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration } from './util' import { toHistoryDuration } from './util'
const topTokens100Query = graphql` const topTokens100Query = graphql`
@ -46,16 +43,26 @@ const topTokens100Query = graphql`
currency currency
} }
} }
project {
logoUrl
}
} }
} }
` `
export enum TokenSortMethod { const tokenSparklineQuery = graphql`
PRICE = 'Price', query TopTokensSparklineQuery($duration: HistoryDuration!, $chain: Chain!) {
PERCENT_CHANGE = 'Change', topTokens(pageSize: 100, page: 1, chain: $chain) {
TOTAL_VALUE_LOCKED = 'TVL', address
VOLUME = 'Volume', market(currency: USD) {
} priceHistory(duration: $duration) {
timestamp
value
}
}
}
}
`
export type PrefetchedTopToken = NonNullable<TopTokens100Query['response']['topTokens']>[number] export type PrefetchedTopToken = NonNullable<TopTokens100Query['response']['topTokens']>[number]
@ -90,7 +97,7 @@ function useSortedTokens(tokens: TopTokens100Query['response']['topTokens'] | un
}, [tokens, sortMethod, sortAscending]) }, [tokens, sortMethod, sortAscending])
} }
function useFilteredTokens(tokens: PrefetchedTopToken[]) { function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']>['topTokens']) {
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const favorites = useAtomValue(favoritesAtom) const favorites = useAtomValue(favoritesAtom)
const showFavorites = useAtomValue(showFavoritesAtom) const showFavorites = useAtomValue(showFavoritesAtom)
@ -121,240 +128,43 @@ function useFilteredTokens(tokens: PrefetchedTopToken[]) {
// Number of items to render in each fetch in infinite scroll. // Number of items to render in each fetch in infinite scroll.
export const PAGE_SIZE = 20 export const PAGE_SIZE = 20
function toContractInput(token: PrefetchedTopToken) { export type TopToken = NonNullable<NonNullable<TopTokens100Query['response']>['topTokens']>[number]
return { export type SparklineMap = { [key: string]: PricePoint[] | undefined }
address: token?.address ?? '',
chain: token?.chain ?? 'ETHEREUM',
}
}
// Map of key: ${HistoryDuration} and value: another Map, of key:${chain} + ${address} and value: TopToken object.
// Acts as a local cache.
let tokensWithPriceHistoryCache: Record<HistoryDuration, Record<string, TopToken>> = {
DAY: {},
HOUR: {},
MAX: {},
MONTH: {},
WEEK: {},
YEAR: {},
'%future added value': {},
}
let cachedChain: Chain | undefined
const resetTokensWithPriceHistoryCache = () => {
tokensWithPriceHistoryCache = {
DAY: {},
HOUR: {},
MAX: {},
MONTH: {},
WEEK: {},
YEAR: {},
'%future added value': {},
}
}
const checkIfAllTokensCached = (duration: HistoryDuration, tokens: PrefetchedTopToken[]) => {
let everyTokenInCache = true
const cachedTokens: TopToken[] = []
const checkCache = (token: PrefetchedTopToken) => {
const tokenCacheKey = !!token ? `${token.chain}${token.address}` : ''
if (tokenCacheKey in tokensWithPriceHistoryCache[duration]) {
cachedTokens.push(tokensWithPriceHistoryCache[duration][tokenCacheKey])
return true
} else {
everyTokenInCache = false
cachedTokens.length = 0
return false
}
}
tokens.every((token) => checkCache(token))
return { everyTokenInCache, cachedTokens }
}
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
interface UseTopTokensReturnValue { interface UseTopTokensReturnValue {
error: Error | undefined
loading: boolean
tokens: TopToken[] | undefined tokens: TopToken[] | undefined
hasMore: boolean sparklines: SparklineMap
loadMoreTokens: () => void
loadingRowCount: number
} }
export function useTopTokens(chain: Chain): UseTopTokensReturnValue { export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const [loadingTokensWithoutPriceHistory, setLoadingTokensWithoutPriceHistory] = useState(true)
const [loadingTokensWithPriceHistory, setLoadingTokensWithPriceHistory] = useState(true)
const [tokens, setTokens] = useState<TopToken[]>()
const [prefetchedData, setPrefetchedData] = useState<PrefetchedTopToken[]>()
if (chain !== cachedChain) {
cachedChain = chain
resetTokensWithPriceHistoryCache()
}
const [page, setPage] = useState(0)
const [error, setError] = useState<Error | undefined>()
const [prefetchedDataDuration, setPrefetchedDataDuration] = useState<HistoryDuration>()
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData))
const { everyTokenInCache, cachedTokens } = useMemo(
() => checkIfAllTokensCached(duration, prefetchedSelectedTokensWithoutPriceHistory),
[duration, prefetchedSelectedTokensWithoutPriceHistory]
)
// loadingRowCount defaults to PAGE_SIZE when no prefetchedData is available yet because the initial load
// count will always be PAGE_SIZE.
const loadingRowCount = useMemo(
() => (prefetchedData ? Math.min(prefetchedSelectedTokensWithoutPriceHistory.length, PAGE_SIZE) : PAGE_SIZE),
[prefetchedSelectedTokensWithoutPriceHistory, prefetchedData]
)
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
const environment = useRelayEnvironment() const environment = useRelayEnvironment()
const [sparklines, setSparklines] = useState<SparklineMap>({})
useMemo(() => {
fetchQuery<TopTokensSparklineQuery>(environment, tokenSparklineQuery, {
duration,
chain,
}).subscribe({
next(data) {
const map: SparklineMap = {}
data.topTokens?.forEach(
(current) => current?.address && (map[current.address] = filterPrices(current?.market?.priceHistory))
)
setSparklines(map)
},
})
}, [chain, duration, environment])
const loadTokensWithoutPriceHistory = useCallback( useEffect(() => {
({ duration, chain }: { duration: HistoryDuration; chain: Chain }) => { setSparklines({})
setTokens([]) }, [duration])
fetchQuery<TopTokens100Query>(
environment, const tokens = useFilteredTokens(
topTokens100Query, useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain }).topTokens
{ duration, chain },
{ fetchPolicy: 'store-or-network' }
).subscribe({
next: (data) => {
if (data?.topTokens) setPrefetchedData([...data?.topTokens])
},
error: setError,
complete: () => {
setLoadingTokensWithoutPriceHistory(false)
setPrefetchedDataDuration(duration)
setLoadingTokensWithPriceHistory(true)
},
})
},
[environment]
) )
// TopTokens should ideally be fetched with usePaginationFragment. The backend does not current support graphql cursors;
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
const loadTokensWithPriceHistory = useCallback(
({
contracts,
appendingTokens,
page,
tokens,
}: {
contracts: ContractInput[]
appendingTokens: boolean
page: number
tokens?: TopToken[]
}) => {
fetchQuery<TopTokens_TokensQuery>(
environment,
tokensQuery,
{ contracts, duration },
{ fetchPolicy: 'store-or-network' }
).subscribe({
next: (data) => {
if (data?.tokens) {
const priceHistoryCacheForCurrentDuration = tokensWithPriceHistoryCache[duration]
data.tokens.map((token) =>
!!token ? (priceHistoryCacheForCurrentDuration[`${token.chain}${token.address}`] = token) : null
)
appendingTokens ? setTokens([...(tokens ?? []), ...data.tokens]) : setTokens([...data.tokens])
setLoadingTokensWithPriceHistory(false)
setPage(page + 1)
}
},
error: setError,
complete: () => setLoadingTokensWithPriceHistory(false),
})
},
[duration, environment]
)
const loadMoreTokens = useCallback(() => {
setLoadingTokensWithPriceHistory(true)
const contracts = prefetchedSelectedTokensWithoutPriceHistory
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
.map(toContractInput)
loadTokensWithPriceHistory({ contracts, appendingTokens: true, page, tokens })
}, [prefetchedSelectedTokensWithoutPriceHistory, page, loadTokensWithPriceHistory, tokens])
// Load tokens from cache when everything is available.
useEffect(() => {
if (everyTokenInCache) {
setTokens(cachedTokens)
setLoadingTokensWithPriceHistory(false)
}
}, [everyTokenInCache, cachedTokens])
// Load new token with price history data when prefetchedSelectedTokensWithoutPriceHistory for current
// duration has already been resolved.
useEffect(() => {
if (!everyTokenInCache) {
setLoadingTokensWithPriceHistory(true)
setTokens([])
if (duration === prefetchedDataDuration) {
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
}
}
}, [
everyTokenInCache,
prefetchedSelectedTokensWithoutPriceHistory,
loadTokensWithPriceHistory,
duration,
prefetchedDataDuration,
])
// Trigger fetching top 100 tokens without price history on first load, and on
// each change of chain or duration.
useEffect(() => {
setLoadingTokensWithoutPriceHistory(true)
loadTokensWithoutPriceHistory({ duration, chain })
}, [chain, duration, loadTokensWithoutPriceHistory])
return { return {
error, tokens: useSortedTokens(tokens),
loading: loadingTokensWithPriceHistory || loadingTokensWithoutPriceHistory, sparklines,
tokens,
hasMore,
loadMoreTokens,
loadingRowCount,
} }
} }
export const tokensQuery = graphql`
query TopTokens_TokensQuery($contracts: [ContractInput!]!, $duration: HistoryDuration!) {
tokens(contracts: $contracts) {
id @required(action: LOG)
name
chain @required(action: LOG)
address @required(action: LOG)
symbol
market(currency: USD) {
totalValueLocked {
value
currency
}
priceHistory(duration: $duration) {
timestamp
value
}
price {
value
currency
}
volume(duration: $duration) {
value
currency
}
pricePercentChange(duration: $duration) {
currency
value
}
}
project {
logoUrl
}
}
}
`

@ -42,7 +42,7 @@ import RemoveLiquidity from './RemoveLiquidity'
import RemoveLiquidityV3 from './RemoveLiquidity/V3' import RemoveLiquidityV3 from './RemoveLiquidity/V3'
import Swap from './Swap' import Swap from './Swap'
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects' import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
import Tokens, { LoadingTokens } from './Tokens' import Tokens from './Tokens'
const TokenDetails = lazy(() => import('./TokenDetails')) const TokenDetails = lazy(() => import('./TokenDetails'))
const Vote = lazy(() => import('./Vote')) const Vote = lazy(() => import('./Vote'))
@ -168,14 +168,7 @@ export default function App() {
<Routes> <Routes>
{tokensFlag === TokensVariant.Enabled && ( {tokensFlag === TokensVariant.Enabled && (
<> <>
<Route <Route path="tokens" element={<Tokens />}>
path="tokens"
element={
<Suspense fallback={<LoadingTokens />}>
<Tokens />
</Suspense>
}
>
<Route path=":chainName" /> <Route path=":chainName" />
</Route> </Route>
<Route <Route

@ -10,10 +10,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar'
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector' import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable' import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { PAGE_SIZE } from 'graphql/data/TopTokens'
import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util' import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useResetAtom } from 'jotai/utils' import { useResetAtom } from 'jotai/utils'
import { useEffect } from 'react' import { Suspense, useEffect, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
@ -76,6 +77,8 @@ const Tokens = () => {
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const connectedChainName = chainIdToBackendName(connectedChainId) const connectedChainName = chainIdToBackendName(connectedChainId)
const [rowCount, setRowCount] = useState(PAGE_SIZE)
useEffect(() => { useEffect(() => {
resetFilterString() resetFilterString()
}, [location, resetFilterString]) }, [location, resetFilterString])
@ -110,7 +113,9 @@ const Tokens = () => {
<SearchBar /> <SearchBar />
</SearchContainer> </SearchContainer>
</FiltersWrapper> </FiltersWrapper>
<TokenTable /> <Suspense fallback={<LoadingTokenTable rowCount={rowCount} />}>
<TokenTable setRowCount={setRowCount} />
</Suspense>
</ExploreContainer> </ExploreContainer>
</Trace> </Trace>
) )