fix: glitchy lazy loading (big jump / unnecessary scroll) (#4776)
* fix glitchy loading * fix initial no tokens state
This commit is contained in:
parent
9859c0b4dd
commit
8c1e41a3a8
@ -72,8 +72,7 @@ export default function TokenTable() {
|
|||||||
|
|
||||||
// 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 { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens, maxFetchable } =
|
const { error, loading, tokens, hasMore, loadMoreTokens, maxFetchable } = useTopTokens(chainName)
|
||||||
useTopTokens(chainName)
|
|
||||||
const showMoreLoadingRows = Boolean(loading && hasMore)
|
const showMoreLoadingRows = Boolean(loading && hasMore)
|
||||||
|
|
||||||
const observer = useRef<IntersectionObserver>()
|
const observer = useRef<IntersectionObserver>()
|
||||||
@ -93,9 +92,9 @@ export default function TokenTable() {
|
|||||||
|
|
||||||
/* loading and error state */
|
/* loading and error state */
|
||||||
if (loading && (!tokens || tokens?.length === 0)) {
|
if (loading && (!tokens || tokens?.length === 0)) {
|
||||||
return <LoadingTokenTable rowCount={Math.min(tokensWithoutPriceHistoryCount, PAGE_SIZE)} />
|
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||||
} else {
|
} else {
|
||||||
if (!tokens) {
|
if (error || !tokens) {
|
||||||
return (
|
return (
|
||||||
<NoTokensState
|
<NoTokensState
|
||||||
message={
|
message={
|
||||||
|
@ -8,8 +8,8 @@ import {
|
|||||||
sortMethodAtom,
|
sortMethodAtom,
|
||||||
} from 'components/Tokens/state'
|
} from 'components/Tokens/state'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
||||||
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
|
import { fetchQuery, useRelayEnvironment } from 'react-relay'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Chain,
|
Chain,
|
||||||
@ -20,10 +20,6 @@ import {
|
|||||||
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
||||||
import { toHistoryDuration } from './util'
|
import { toHistoryDuration } from './util'
|
||||||
|
|
||||||
export function usePrefetchTopTokens(duration: HistoryDuration, chain: Chain) {
|
|
||||||
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
|
|
||||||
}
|
|
||||||
|
|
||||||
const topTokens100Query = graphql`
|
const topTokens100Query = graphql`
|
||||||
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
|
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
|
||||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||||
@ -166,29 +162,48 @@ const checkIfAllTokensCached = (duration: HistoryDuration, tokens: PrefetchedTop
|
|||||||
|
|
||||||
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
|
export type TopToken = NonNullable<TopTokens_TokensQuery['response']['tokens']>[number]
|
||||||
interface UseTopTokensReturnValue {
|
interface UseTopTokensReturnValue {
|
||||||
|
error: Error | undefined
|
||||||
loading: boolean
|
loading: boolean
|
||||||
tokens: TopToken[] | undefined
|
tokens: TopToken[] | undefined
|
||||||
tokensWithoutPriceHistoryCount: number
|
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
loadMoreTokens: () => void
|
loadMoreTokens: () => void
|
||||||
maxFetchable: number
|
maxFetchable: number
|
||||||
}
|
}
|
||||||
export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||||
const [loading, setLoading] = useState(true)
|
const [loadingTokensWithoutPriceHistory, setLoadingTokensWithoutPriceHistory] = useState(true)
|
||||||
|
const [loadingTokensWithPriceHistory, setLoadingTokensWithPriceHistory] = useState(true)
|
||||||
const [tokens, setTokens] = useState<TopToken[]>()
|
const [tokens, setTokens] = useState<TopToken[]>()
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const prefetchedData = usePrefetchTopTokens(duration, chain)
|
const [error, setError] = useState<Error | undefined>()
|
||||||
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
|
const [prefetchedData, setPrefetchedData] = useState<PrefetchedTopToken[]>([])
|
||||||
|
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData))
|
||||||
const maxFetchable = useMemo(
|
const maxFetchable = useMemo(
|
||||||
() => prefetchedSelectedTokensWithoutPriceHistory.length,
|
() => prefetchedSelectedTokensWithoutPriceHistory.length,
|
||||||
[prefetchedSelectedTokensWithoutPriceHistory]
|
[prefetchedSelectedTokensWithoutPriceHistory]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
|
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
|
||||||
|
|
||||||
const environment = useRelayEnvironment()
|
const environment = useRelayEnvironment()
|
||||||
|
|
||||||
|
const loadTokensWithoutPriceHistory = useCallback(
|
||||||
|
({ duration, chain }: { duration: HistoryDuration; chain: Chain }) => {
|
||||||
|
fetchQuery<TopTokens100Query>(
|
||||||
|
environment,
|
||||||
|
topTokens100Query,
|
||||||
|
{ duration, chain },
|
||||||
|
{ fetchPolicy: 'store-or-network' }
|
||||||
|
).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
if (data?.topTokens) setPrefetchedData([...data?.topTokens])
|
||||||
|
},
|
||||||
|
error: setError,
|
||||||
|
complete: () => setLoadingTokensWithoutPriceHistory(false),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[environment]
|
||||||
|
)
|
||||||
|
|
||||||
// TopTokens should ideally be fetched with usePaginationFragment. The backend does not current support graphql cursors;
|
// 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
|
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
|
||||||
const loadTokensWithPriceHistory = useCallback(
|
const loadTokensWithPriceHistory = useCallback(
|
||||||
@ -208,25 +223,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
|||||||
tokensQuery,
|
tokensQuery,
|
||||||
{ contracts, duration },
|
{ contracts, duration },
|
||||||
{ fetchPolicy: 'store-or-network' }
|
{ fetchPolicy: 'store-or-network' }
|
||||||
)
|
).subscribe({
|
||||||
.toPromise()
|
next: (data) => {
|
||||||
.then((data) => {
|
|
||||||
if (data?.tokens) {
|
if (data?.tokens) {
|
||||||
const priceHistoryCacheForCurrentDuration = tokensWithPriceHistoryCache[duration]
|
const priceHistoryCacheForCurrentDuration = tokensWithPriceHistoryCache[duration]
|
||||||
data.tokens.map((token) =>
|
data.tokens.map((token) =>
|
||||||
!!token ? (priceHistoryCacheForCurrentDuration[`${token.chain}${token.address}`] = token) : null
|
!!token ? (priceHistoryCacheForCurrentDuration[`${token.chain}${token.address}`] = token) : null
|
||||||
)
|
)
|
||||||
appendingTokens ? setTokens([...(tokens ?? []), ...data.tokens]) : setTokens([...data.tokens])
|
appendingTokens ? setTokens([...(tokens ?? []), ...data.tokens]) : setTokens([...data.tokens])
|
||||||
setLoading(false)
|
setLoadingTokensWithPriceHistory(false)
|
||||||
setPage(page + 1)
|
setPage(page + 1)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
error: setError,
|
||||||
|
complete: () => setLoadingTokensWithPriceHistory(false),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[duration, environment]
|
[duration, environment]
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadMoreTokens = useCallback(() => {
|
const loadMoreTokens = useCallback(() => {
|
||||||
setLoading(true)
|
setLoadingTokensWithPriceHistory(true)
|
||||||
const contracts = prefetchedSelectedTokensWithoutPriceHistory
|
const contracts = prefetchedSelectedTokensWithoutPriceHistory
|
||||||
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
|
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
|
||||||
.map(toContractInput)
|
.map(toContractInput)
|
||||||
@ -241,21 +258,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
|||||||
)
|
)
|
||||||
if (everyTokenInCache) {
|
if (everyTokenInCache) {
|
||||||
setTokens(cachedTokens)
|
setTokens(cachedTokens)
|
||||||
setLoading(false)
|
setLoadingTokensWithPriceHistory(false)
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
setLoading(true)
|
setLoadingTokensWithPriceHistory(true)
|
||||||
setTokens([])
|
setTokens([])
|
||||||
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
|
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
|
||||||
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
|
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
|
||||||
}
|
}
|
||||||
}, [loadTokensWithPriceHistory, prefetchedSelectedTokensWithoutPriceHistory, duration])
|
}, [loadTokensWithPriceHistory, prefetchedSelectedTokensWithoutPriceHistory, duration])
|
||||||
|
|
||||||
|
// 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 {
|
||||||
loading,
|
error,
|
||||||
|
loading: loadingTokensWithPriceHistory || loadingTokensWithoutPriceHistory,
|
||||||
tokens,
|
tokens,
|
||||||
hasMore,
|
hasMore,
|
||||||
tokensWithoutPriceHistoryCount: prefetchedSelectedTokensWithoutPriceHistory.length,
|
|
||||||
loadMoreTokens,
|
loadMoreTokens,
|
||||||
maxFetchable,
|
maxFetchable,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user