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 // 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,
} }