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:
parent
351f66a83e
commit
d8677d8a6d
@ -1,14 +1,20 @@
|
||||
import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import { curveCardinal, scaleLinear } from 'd3'
|
||||
import { filterPrices } from 'graphql/data/Token'
|
||||
import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { PricePoint } from 'graphql/data/Token'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import React from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { memo } from 'react'
|
||||
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'
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
const LoadingContainer = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
interface SparklineChartProps {
|
||||
width: number
|
||||
@ -16,15 +22,32 @@ interface SparklineChartProps {
|
||||
tokenData: TopToken
|
||||
pricePercentChange: number | undefined | null
|
||||
timePeriod: TimePeriod
|
||||
sparklineMap: SparklineMap
|
||||
}
|
||||
|
||||
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) {
|
||||
function _SparklineChart({
|
||||
width,
|
||||
height,
|
||||
tokenData,
|
||||
pricePercentChange,
|
||||
timePeriod,
|
||||
sparklineMap,
|
||||
}: SparklineChartProps) {
|
||||
const theme = useTheme()
|
||||
// for sparkline
|
||||
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? []
|
||||
const hasData = pricePoints.length !== 0
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
|
||||
|
||||
// Don't display if there's one or less pricepoints
|
||||
if (!pricePoints || pricePoints.length <= 1) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<SparkLineLoadingBubble />
|
||||
</LoadingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const startingPrice = pricePoints[0]
|
||||
const endingPrice = pricePoints[pricePoints.length - 1]
|
||||
const widthScale = scaleLinear()
|
||||
.domain(
|
||||
// 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 { TokenSortMethod } from 'graphql/data/TopTokens'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { textFadeIn } from 'theme/animations'
|
||||
import { formatDollar } from 'utils/formatDollarAmt'
|
||||
|
||||
import { TokenSortMethod } from '../state'
|
||||
import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow'
|
||||
import InfoTip from './InfoTip'
|
||||
|
||||
|
@ -6,7 +6,7 @@ import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
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 { useAtomValue } from 'jotai/utils'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
@ -30,6 +30,7 @@ import {
|
||||
filterTimeAtom,
|
||||
sortAscendingAtom,
|
||||
sortMethodAtom,
|
||||
TokenSortMethod,
|
||||
useIsFavorited,
|
||||
useSetSortMethod,
|
||||
useToggleFavorite,
|
||||
@ -310,7 +311,7 @@ const IconLoadingBubble = styled(LoadingBubble)`
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
`
|
||||
const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
height: 4px;
|
||||
`
|
||||
|
||||
@ -395,7 +396,7 @@ export function TokenRow({
|
||||
marketCap: ReactNode
|
||||
price: ReactNode
|
||||
percentChange: ReactNode
|
||||
sparkLine: ReactNode
|
||||
sparkLine?: ReactNode
|
||||
tokenInfo: ReactNode
|
||||
volume: ReactNode
|
||||
last?: boolean
|
||||
@ -466,6 +467,7 @@ interface LoadedRowProps {
|
||||
tokenListIndex: number
|
||||
tokenListLength: number
|
||||
token: NonNullable<TopToken>
|
||||
sparklineMap: SparklineMap
|
||||
}
|
||||
|
||||
/* 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 toggleFavorite = useToggleFavorite(tokenAddress)
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
|
||||
const filterNetwork = lowercaseChainName.toUpperCase()
|
||||
@ -515,7 +518,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
}
|
||||
listNumber={tokenListIndex + 1}
|
||||
listNumber={sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
@ -558,15 +561,18 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
sparkLine={
|
||||
<SparkLine>
|
||||
<ParentSize>
|
||||
{({ width, height }) => (
|
||||
<SparklineChart
|
||||
width={width}
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
)}
|
||||
{({ width, height }) =>
|
||||
props.sparklineMap && (
|
||||
<SparklineChart
|
||||
width={width}
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
sparklineMap={props.sparklineMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ParentSize>
|
||||
</SparkLine>
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { showFavoritesAtom } from 'components/Tokens/state'
|
||||
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
|
||||
import { validateUrlChainParam } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, useCallback, useRef } from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
@ -11,8 +11,6 @@ import styled from 'styled-components/macro'
|
||||
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow'
|
||||
|
||||
const LOADING_ROWS_COUNT = 3
|
||||
|
||||
const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -58,13 +56,19 @@ function NoTokensState({ message }: { message: ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingRows = (rowCount?: number) =>
|
||||
Array(rowCount ?? PAGE_SIZE)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
return <LoadingRow key={index} />
|
||||
})
|
||||
const LoadingMoreRows = LoadingRows(LOADING_ROWS_COUNT)
|
||||
const LoadingRowsWrapper = styled.div`
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const LoadingRows = (rowCount?: number) => (
|
||||
<LoadingRowsWrapper>
|
||||
{Array(rowCount ?? PAGE_SIZE)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
return <LoadingRow key={index} />
|
||||
})}
|
||||
</LoadingRowsWrapper>
|
||||
)
|
||||
|
||||
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
|
||||
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)
|
||||
|
||||
// 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 { error, loading, tokens, hasMore, loadMoreTokens, loadingRowCount } = useTopTokens(chainName)
|
||||
const showMoreLoadingRows = Boolean(loading && hasMore)
|
||||
|
||||
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]
|
||||
)
|
||||
const { tokens, sparklines } = useTopTokens(chainName)
|
||||
setRowCount(tokens?.length ?? PAGE_SIZE)
|
||||
|
||||
/* loading and error state */
|
||||
if (loading && (!tokens || tokens?.length === 0)) {
|
||||
return <LoadingTokenTable rowCount={loadingRowCount} />
|
||||
if (!tokens) {
|
||||
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 {
|
||||
if (error || !tokens) {
|
||||
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 {
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenDataContainer>
|
||||
{tokens.map(
|
||||
(token, index) =>
|
||||
token && (
|
||||
<LoadedRow
|
||||
key={token?.address}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={tokens.length}
|
||||
token={token}
|
||||
sparklineMap={sparklines}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</TokenDataContainer>
|
||||
</GridContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { TokenSortMethod } from 'graphql/data/TopTokens'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
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 showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
|
||||
export const filterStringAtom = atomWithReset<string>('')
|
||||
|
@ -6,18 +6,15 @@ import {
|
||||
showFavoritesAtom,
|
||||
sortAscendingAtom,
|
||||
sortMethodAtom,
|
||||
TokenSortMethod,
|
||||
} from 'components/Tokens/state'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useRelayEnvironment } from 'react-relay'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
|
||||
|
||||
import {
|
||||
Chain,
|
||||
ContractInput,
|
||||
HistoryDuration,
|
||||
TopTokens_TokensQuery,
|
||||
} from './__generated__/TopTokens_TokensQuery.graphql'
|
||||
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
||||
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
||||
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
|
||||
import { filterPrices, PricePoint } from './Token'
|
||||
import { toHistoryDuration } from './util'
|
||||
|
||||
const topTokens100Query = graphql`
|
||||
@ -46,16 +43,26 @@ const topTokens100Query = graphql`
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export enum TokenSortMethod {
|
||||
PRICE = 'Price',
|
||||
PERCENT_CHANGE = 'Change',
|
||||
TOTAL_VALUE_LOCKED = 'TVL',
|
||||
VOLUME = 'Volume',
|
||||
}
|
||||
const tokenSparklineQuery = graphql`
|
||||
query TopTokensSparklineQuery($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
address
|
||||
market(currency: USD) {
|
||||
priceHistory(duration: $duration) {
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export type PrefetchedTopToken = NonNullable<TopTokens100Query['response']['topTokens']>[number]
|
||||
|
||||
@ -90,7 +97,7 @@ function useSortedTokens(tokens: TopTokens100Query['response']['topTokens'] | un
|
||||
}, [tokens, sortMethod, sortAscending])
|
||||
}
|
||||
|
||||
function useFilteredTokens(tokens: PrefetchedTopToken[]) {
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']>['topTokens']) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const favorites = useAtomValue(favoritesAtom)
|
||||
const showFavorites = useAtomValue(showFavoritesAtom)
|
||||
@ -121,240 +128,43 @@ function useFilteredTokens(tokens: PrefetchedTopToken[]) {
|
||||
// Number of items to render in each fetch in infinite scroll.
|
||||
export const PAGE_SIZE = 20
|
||||
|
||||
function toContractInput(token: PrefetchedTopToken) {
|
||||
return {
|
||||
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]
|
||||
export type TopToken = NonNullable<NonNullable<TopTokens100Query['response']>['topTokens']>[number]
|
||||
export type SparklineMap = { [key: string]: PricePoint[] | undefined }
|
||||
interface UseTopTokensReturnValue {
|
||||
error: Error | undefined
|
||||
loading: boolean
|
||||
tokens: TopToken[] | undefined
|
||||
hasMore: boolean
|
||||
loadMoreTokens: () => void
|
||||
loadingRowCount: number
|
||||
sparklines: SparklineMap
|
||||
}
|
||||
|
||||
export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
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 [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(
|
||||
({ duration, chain }: { duration: HistoryDuration; chain: Chain }) => {
|
||||
setTokens([])
|
||||
fetchQuery<TopTokens100Query>(
|
||||
environment,
|
||||
topTokens100Query,
|
||||
{ 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]
|
||||
useEffect(() => {
|
||||
setSparklines({})
|
||||
}, [duration])
|
||||
|
||||
const tokens = useFilteredTokens(
|
||||
useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain }).topTokens
|
||||
)
|
||||
|
||||
// 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 {
|
||||
error,
|
||||
loading: loadingTokensWithPriceHistory || loadingTokensWithoutPriceHistory,
|
||||
tokens,
|
||||
hasMore,
|
||||
loadMoreTokens,
|
||||
loadingRowCount,
|
||||
tokens: useSortedTokens(tokens),
|
||||
sparklines,
|
||||
}
|
||||
}
|
||||
|
||||
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 Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
import Tokens, { LoadingTokens } from './Tokens'
|
||||
import Tokens from './Tokens'
|
||||
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
@ -168,14 +168,7 @@ export default function App() {
|
||||
<Routes>
|
||||
{tokensFlag === TokensVariant.Enabled && (
|
||||
<>
|
||||
<Route
|
||||
path="tokens"
|
||||
element={
|
||||
<Suspense fallback={<LoadingTokens />}>
|
||||
<Tokens />
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
<Route path="tokens" element={<Tokens />}>
|
||||
<Route path=":chainName" />
|
||||
</Route>
|
||||
<Route
|
||||
|
@ -10,10 +10,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar'
|
||||
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
|
||||
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
|
||||
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
|
||||
import { PAGE_SIZE } from 'graphql/data/TopTokens'
|
||||
import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { useResetAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
@ -76,6 +77,8 @@ const Tokens = () => {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const connectedChainName = chainIdToBackendName(connectedChainId)
|
||||
|
||||
const [rowCount, setRowCount] = useState(PAGE_SIZE)
|
||||
|
||||
useEffect(() => {
|
||||
resetFilterString()
|
||||
}, [location, resetFilterString])
|
||||
@ -110,7 +113,9 @@ const Tokens = () => {
|
||||
<SearchBar />
|
||||
</SearchContainer>
|
||||
</FiltersWrapper>
|
||||
<TokenTable />
|
||||
<Suspense fallback={<LoadingTokenTable rowCount={rowCount} />}>
|
||||
<TokenTable setRowCount={setRowCount} />
|
||||
</Suspense>
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user