Commit d8677d8a authored by cartcrom's avatar cartcrom Committed by GitHub

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: default avatarConnor McEwen <connor.mcewen@gmail.com>
parent 351f66a8
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)
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 }) => (
{({ 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)
const LoadingRowsWrapper = styled.div`
margin-top: 8px;
`
const LoadingRows = (rowCount?: number) => (
<LoadingRowsWrapper>
{Array(rowCount ?? PAGE_SIZE)
.fill(null)
.map((_, index) => {
return <LoadingRow key={index} />
})
const LoadingMoreRows = LoadingRows(LOADING_ROWS_COUNT)
})}
</LoadingRowsWrapper>
)
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
return (
......@@ -75,34 +79,16 @@ 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} />
} else {
if (error || !tokens) {
if (!tokens) {
return (
<NoTokensState
message={
......@@ -121,7 +107,6 @@ export default function TokenTable() {
)
} else {
return (
<>
<GridContainer>
<HeaderRow />
<TokenDataContainer>
......@@ -133,15 +118,12 @@ export default function TokenTable() {
tokenListIndex={index}
tokenListLength={tokens.length}
token={token}
ref={index + 1 === tokens.length ? lastTokenRef : undefined}
sparklineMap={sparklines}
/>
)
)}
{showMoreLoadingRows && LoadingMoreRows}
</TokenDataContainer>
</GridContainer>
</>
)
}
}
}
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>('')
......
This diff is collapsed.
......@@ -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>
)
......
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