Commit 029f3acb authored by lynn's avatar lynn Committed by GitHub

fix: fix explore table filtering and sorting bugs (#4705)

* fix explore table bugs

* temp

* fix search bar incongruency when navigating to other tab + remove flickering data

* add local cache

* more clear names

* add back useTopTokens return type interface

* respond to comments and dedup repeated code

* respond to cmcewen comments
parent 0b9fda5b
......@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import searchIcon from 'assets/svg/search.svg'
import xIcon from 'assets/svg/x.svg'
import useDebounce from 'hooks/useDebounce'
import { useUpdateAtom } from 'jotai/utils'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
......@@ -59,10 +59,15 @@ const SearchInput = styled.input`
`
export default function SearchBar() {
const [localFilterString, setLocalFilterString] = useState('')
const currentString = useAtomValue(filterStringAtom)
const [localFilterString, setLocalFilterString] = useState(currentString)
const setFilterString = useUpdateAtom(filterStringAtom)
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
useEffect(() => {
setLocalFilterString(currentString)
}, [currentString])
useEffect(() => {
setFilterString(debouncedLocalFilterString)
}, [debouncedLocalFilterString, setFilterString])
......
......@@ -38,8 +38,6 @@ import {
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { DISPLAYS } from './TimeSelector'
export const MAX_TOKENS_TO_LOAD = 100
const Cell = styled.div`
display: flex;
align-items: center;
......@@ -520,7 +518,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
}
listNumber={sortAscending ? MAX_TOKENS_TO_LOAD - tokenListIndex : tokenListIndex + 1}
listNumber={sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1}
tokenInfo={
<ClickableName>
<LogoContainer>
......
import { Trans } from '@lingui/macro'
import { showFavoritesAtom } from 'components/Tokens/state'
import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
import { useAtomValue } from 'jotai/utils'
import { ReactNode, useCallback, useRef } from 'react'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { HeaderRow, LoadedRow, LoadingRow, MAX_TOKENS_TO_LOAD } from './TokenRow'
import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow'
const LOADING_ROWS_COUNT = 3
const ROWS_PER_PAGE_FETCH = 20
const GridContainer = styled.div`
display: flex;
......@@ -55,13 +54,13 @@ function NoTokensState({ message }: { message: ReactNode }) {
}
const LoadingMoreRows = Array(LOADING_ROWS_COUNT).fill(<LoadingRow />)
const InitialLoadingRows = Array(ROWS_PER_PAGE_FETCH).fill(<LoadingRow />)
const LoadingRows = (rowCount?: number) => Array(rowCount ?? PAGE_SIZE).fill(<LoadingRow />)
export function LoadingTokenTable() {
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
return (
<GridContainer>
<HeaderRow />
<TokenDataContainer>{InitialLoadingRows}</TokenDataContainer>
<TokenDataContainer>{LoadingRows(rowCount)}</TokenDataContainer>
</GridContainer>
)
}
......@@ -70,9 +69,8 @@ export default function TokenTable() {
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 prefetchedTokens = usePrefetchTopTokens()
const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
const hasMore = !tokens || tokens.length < MAX_TOKENS_TO_LOAD
const { loading, tokens, tokensWithoutPriceHistoryCount, hasMore, loadMoreTokens } = useTopTokens()
const showMoreLoadingRows = Boolean(loading && hasMore)
const observer = useRef<IntersectionObserver>()
const lastTokenRef = useCallback(
......@@ -91,7 +89,7 @@ export default function TokenTable() {
/* loading and error state */
if (loading && (!tokens || tokens?.length === 0)) {
return <LoadingTokenTable />
return <LoadingTokenTable rowCount={Math.min(tokensWithoutPriceHistoryCount, PAGE_SIZE)} />
} else {
if (!tokens) {
return (
......@@ -118,14 +116,14 @@ export default function TokenTable() {
<TokenDataContainer>
{tokens.map((token, index) => (
<LoadedRow
key={token?.name}
key={token?.address}
tokenListIndex={index}
tokenListLength={tokens?.length ?? 0}
token={token}
ref={tokens.length === index + 1 ? lastTokenRef : undefined}
ref={index + 1 === tokens.length ? lastTokenRef : undefined}
/>
))}
{loading && LoadingMoreRows}
{showMoreLoadingRows && LoadingMoreRows}
</TokenDataContainer>
</GridContainer>
</>
......
......@@ -8,18 +8,17 @@ import {
sortMethodAtom,
} from 'components/Tokens/state'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { ContractInput, TopTokens_TokensQuery } from './__generated__/TopTokens_TokensQuery.graphql'
import { ContractInput, HistoryDuration, TopTokens_TokensQuery } from './__generated__/TopTokens_TokensQuery.graphql'
import type { TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { toHistoryDuration, useCurrentChainName } from './util'
import { toHistoryDuration } from './util'
import { useCurrentChainName } from './util'
export function usePrefetchTopTokens() {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
export function usePrefetchTopTokens(duration: HistoryDuration) {
const chain = useCurrentChainName()
const args = useMemo(() => ({ chain, duration }), [chain, duration])
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, args)
return useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
}
const topTokens100Query = graphql`
......@@ -120,7 +119,8 @@ function useFilteredTokens(tokens: PrefetchedTopToken[]) {
}, [tokens, showFavorites, lowercaseFilterString, favorites])
}
const PAGE_SIZE = 20
// Number of items to render in each fetch in infinite scroll.
export const PAGE_SIZE = 20
function toContractInput(token: PrefetchedTopToken) {
return {
......@@ -129,63 +129,114 @@ function toContractInput(token: PrefetchedTopToken) {
}
}
// Map of key: ${chain} + ${address} and value: TopToken object.
// Acts as a local cache.
const tokensWithPriceHistoryCache: Record<string, TopToken> = {}
const checkIfAllTokensCached = (tokens: PrefetchedTopToken[]) => {
let everyTokenInCache = true
const cachedTokens: TopToken[] = []
const checkCache = (token: PrefetchedTopToken) => {
const tokenCacheKey = !!token ? `${token.chain}${token.address}` : ''
if (tokenCacheKey in tokensWithPriceHistoryCache) {
cachedTokens.push(tokensWithPriceHistoryCache[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 {
loading: boolean
tokens: TopToken[]
tokens: TopToken[] | undefined
tokensWithoutPriceHistoryCount: number
hasMore: boolean
loadMoreTokens: () => void
}
export function useTopTokens(prefetchedData: TopTokens100Query['response']): UseTopTokensReturnValue {
export function useTopTokens(): UseTopTokensReturnValue {
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const environment = useRelayEnvironment()
const [tokens, setTokens] = useState<TopToken[]>([])
const [page, setPage] = useState(0)
const [loading, setLoading] = useState(true)
const [tokens, setTokens] = useState<TopToken[]>()
const [page, setPage] = useState(0)
const prefetchedData = usePrefetchTopTokens(duration)
const prefetchedSelectedTokensWithoutPriceHistory = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const appendTokens = useCallback(
(newTokens: TopToken[]) => {
setTokens(
Object.values(
tokens
.concat(newTokens)
.reduce((acc, token) => (token?.address ? { ...acc, [token.address]: token } : acc), {})
)
)
},
[tokens]
)
const loadMoreTokens = useCallback(() => setPage(page + 1), [page])
const hasMore = !tokens || tokens.length < prefetchedSelectedTokensWithoutPriceHistory.length
const environment = useRelayEnvironment()
// 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 prefetchedSelectedTokens = useFilteredTokens(useSortedTokens(prefetchedData.topTokens))
const contracts: ContractInput[] = useMemo(
() => prefetchedSelectedTokens.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map(toContractInput),
[page, prefetchedSelectedTokens]
)
useEffect(() => {
const subscription = fetchQuery<TopTokens_TokensQuery>(
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({
start() {
setLoading(true)
},
complete() {
)
.toPromise()
.then((data) => {
if (data?.tokens) {
data.tokens.map((token) =>
!!token ? (tokensWithPriceHistoryCache[`${token.chain}${token.address}`] = token) : null
)
appendingTokens ? setTokens([...(tokens ?? []), ...data.tokens]) : setTokens([...data.tokens])
setLoading(false)
},
next(data) {
appendTokens(data.tokens as TopToken[])
},
setPage(page + 1)
}
})
return subscription.unsubscribe
}, [appendTokens, contracts, duration, environment])
},
[duration, environment]
)
const loadMoreTokens = useCallback(() => {
setLoading(true)
const contracts = prefetchedSelectedTokensWithoutPriceHistory
.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE)
.map(toContractInput)
loadTokensWithPriceHistory({ contracts, appendingTokens: true, page, tokens })
}, [prefetchedSelectedTokensWithoutPriceHistory, page, loadTokensWithPriceHistory, tokens])
return { loading, tokens: useFilteredTokens(useSortedTokens(tokens)) as TopToken[], loadMoreTokens }
// Reset count when filters are changed
useLayoutEffect(() => {
const { everyTokenInCache, cachedTokens } = checkIfAllTokensCached(prefetchedSelectedTokensWithoutPriceHistory)
if (everyTokenInCache) {
setTokens(cachedTokens)
setLoading(false)
return
} else {
setLoading(true)
setTokens([])
const contracts = prefetchedSelectedTokensWithoutPriceHistory.slice(0, PAGE_SIZE).map(toContractInput)
loadTokensWithPriceHistory({ contracts, appendingTokens: false, page: 0 })
}
}, [loadTokensWithPriceHistory, prefetchedSelectedTokensWithoutPriceHistory])
return {
loading,
tokens,
hasMore,
tokensWithoutPriceHistoryCount: prefetchedSelectedTokensWithoutPriceHistory.length,
loadMoreTokens,
}
}
export const tokensQuery = graphql`
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment