fix: glitchy lazy loading (big jump / unnecessary scroll) (#4776)

* fix glitchy loading

* fix initial no tokens state
This commit is contained in:
lynn 2022-09-30 17:23:05 -04:00 committed by GitHub
parent 9859c0b4dd
commit 8c1e41a3a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 26 deletions

@ -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
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens, maxFetchable } =
useTopTokens(chainName)
const { error, loading, tokens, hasMore, loadMoreTokens, maxFetchable } = useTopTokens(chainName)
const showMoreLoadingRows = Boolean(loading && hasMore)
const observer = useRef<IntersectionObserver>()
@ -93,9 +92,9 @@ export default function TokenTable() {
/* loading and error state */
if (loading && (!tokens || tokens?.length === 0)) {
return <LoadingTokenTable rowCount={Math.min(tokensWithoutPriceHistoryCount, PAGE_SIZE)} />
return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else {
if (!tokens) {
if (error || !tokens) {
return (
<NoTokensState
message={

@ -8,8 +8,8 @@ import {
sortMethodAtom,
} from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { fetchQuery, useRelayEnvironment } from 'react-relay'
import {
Chain,
@ -20,10 +20,6 @@ import {
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration } from './util'
export function usePrefetchTopTokens(duration: HistoryDuration, chain: Chain) {
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
}
const topTokens100Query = graphql`
query TopTokens100Query($duration: HistoryDuration!, $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]
interface UseTopTokensReturnValue {
error: Error | undefined
loading: boolean
tokens: TopToken[] | undefined
tokensWithoutPriceHistoryCount: number
hasMore: boolean
loadMoreTokens: () => void
maxFetchable: number
}
export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
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 [page, setPage] = useState(0)
const prefetchedData = usePrefetchTopTokens(duration, chain)
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const [error, setError] = useState<Error | undefined>()
const [prefetchedData, setPrefetchedData] = useState<PrefetchedTopToken[]>([])
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData))
const maxFetchable = useMemo(
() => prefetchedSelectedTokensWithoutPriceHistory.length,
[prefetchedSelectedTokensWithoutPriceHistory]
)
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
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;
// in the meantime, fetchQuery is used, as other relay hooks do not allow the refreshing and lazy loading we need
const loadTokensWithPriceHistory = useCallback(
@ -208,25 +223,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
tokensQuery,
{ contracts, duration },
{ fetchPolicy: 'store-or-network' }
)
.toPromise()
.then((data) => {
).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])
setLoading(false)
setLoadingTokensWithPriceHistory(false)
setPage(page + 1)
}
},
error: setError,
complete: () => setLoadingTokensWithPriceHistory(false),
})
},
[duration, environment]
)
const loadMoreTokens = useCallback(() => {
setLoading(true)
setLoadingTokensWithPriceHistory(true)
const contracts = prefetchedSelectedTokensWithoutPriceHistory
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
.map(toContractInput)
@ -241,21 +258,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
)
if (everyTokenInCache) {
setTokens(cachedTokens)
setLoading(false)
return
setLoadingTokensWithPriceHistory(false)
} else {
setLoading(true)
setLoadingTokensWithPriceHistory(true)
setTokens([])
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
}
}, [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 {
loading,
error,
loading: loadingTokensWithPriceHistory || loadingTokensWithoutPriceHistory,
tokens,
hasMore,
tokensWithoutPriceHistoryCount: prefetchedSelectedTokensWithoutPriceHistory.length,
loadMoreTokens,
maxFetchable,
}