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 { curveCardinal, scaleLinear } from 'd3'
import { filterPrices } from 'graphql/data/Token' import { PricePoint } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import React from 'react' import { memo } from 'react'
import { useTheme } from 'styled-components/macro' 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' 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 { interface SparklineChartProps {
width: number width: number
...@@ -16,15 +22,32 @@ interface SparklineChartProps { ...@@ -16,15 +22,32 @@ interface SparklineChartProps {
tokenData: TopToken tokenData: TopToken
pricePercentChange: number | undefined | null pricePercentChange: number | undefined | null
timePeriod: TimePeriod timePeriod: TimePeriod
sparklineMap: SparklineMap
} }
function SparklineChart({ width, height, tokenData, pricePercentChange, timePeriod }: SparklineChartProps) { function _SparklineChart({
width,
height,
tokenData,
pricePercentChange,
timePeriod,
sparklineMap,
}: SparklineChartProps) {
const theme = useTheme() const theme = useTheme()
// for sparkline // for sparkline
const pricePoints = filterPrices(tokenData?.market?.priceHistory) ?? [] const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null
const hasData = pricePoints.length !== 0
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY // Don't display if there's one or less pricepoints
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY if (!pricePoints || pricePoints.length <= 1) {
return (
<LoadingContainer>
<SparkLineLoadingBubble />
</LoadingContainer>
)
}
const startingPrice = pricePoints[0]
const endingPrice = pricePoints[pricePoints.length - 1]
const widthScale = scaleLinear() const widthScale = scaleLinear()
.domain( .domain(
// the range of possible input values // the range of possible input values
...@@ -52,4 +75,4 @@ function SparklineChart({ width, height, tokenData, pricePercentChange, timePeri ...@@ -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 { Trans } from '@lingui/macro'
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/animations' import { textFadeIn } from 'theme/animations'
import { formatDollar } from 'utils/formatDollarAmt' import { formatDollar } from 'utils/formatDollarAmt'
import { TokenSortMethod } from '../state'
import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow' import { HEADER_DESCRIPTIONS } from '../TokenTable/TokenRow'
import InfoTip from './InfoTip' import InfoTip from './InfoTip'
......
...@@ -6,7 +6,7 @@ import SparklineChart from 'components/Charts/SparklineChart' ...@@ -6,7 +6,7 @@ import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' 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 { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ForwardedRef, forwardRef } from 'react' import { ForwardedRef, forwardRef } from 'react'
...@@ -30,6 +30,7 @@ import { ...@@ -30,6 +30,7 @@ import {
filterTimeAtom, filterTimeAtom,
sortAscendingAtom, sortAscendingAtom,
sortMethodAtom, sortMethodAtom,
TokenSortMethod,
useIsFavorited, useIsFavorited,
useSetSortMethod, useSetSortMethod,
useToggleFavorite, useToggleFavorite,
...@@ -310,7 +311,7 @@ const IconLoadingBubble = styled(LoadingBubble)` ...@@ -310,7 +311,7 @@ const IconLoadingBubble = styled(LoadingBubble)`
border-radius: 50%; border-radius: 50%;
width: 24px; width: 24px;
` `
const SparkLineLoadingBubble = styled(LongLoadingBubble)` export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
height: 4px; height: 4px;
` `
...@@ -395,7 +396,7 @@ export function TokenRow({ ...@@ -395,7 +396,7 @@ export function TokenRow({
marketCap: ReactNode marketCap: ReactNode
price: ReactNode price: ReactNode
percentChange: ReactNode percentChange: ReactNode
sparkLine: ReactNode sparkLine?: ReactNode
tokenInfo: ReactNode tokenInfo: ReactNode
volume: ReactNode volume: ReactNode
last?: boolean last?: boolean
...@@ -466,6 +467,7 @@ interface LoadedRowProps { ...@@ -466,6 +467,7 @@ interface LoadedRowProps {
tokenListIndex: number tokenListIndex: number
tokenListLength: number tokenListLength: number
token: NonNullable<TopToken> token: NonNullable<TopToken>
sparklineMap: SparklineMap
} }
/* Loaded State: row component with token information */ /* Loaded State: row component with token information */
...@@ -477,6 +479,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -477,6 +479,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
const isFavorited = useIsFavorited(tokenAddress) const isFavorited = useIsFavorited(tokenAddress)
const toggleFavorite = useToggleFavorite(tokenAddress) const toggleFavorite = useToggleFavorite(tokenAddress)
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum' const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase() const filterNetwork = lowercaseChainName.toUpperCase()
...@@ -515,7 +518,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -515,7 +518,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
<FavoriteIcon isFavorited={isFavorited} /> <FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited> </ClickFavorited>
} }
listNumber={tokenListIndex + 1} listNumber={sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1}
tokenInfo={ tokenInfo={
<ClickableName> <ClickableName>
<LogoContainer> <LogoContainer>
...@@ -558,15 +561,18 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT ...@@ -558,15 +561,18 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
sparkLine={ sparkLine={
<SparkLine> <SparkLine>
<ParentSize> <ParentSize>
{({ width, height }) => ( {({ width, height }) =>
<SparklineChart props.sparklineMap && (
width={width} <SparklineChart
height={height} width={width}
tokenData={token} height={height}
pricePercentChange={token.market?.pricePercentChange?.value} tokenData={token}
timePeriod={timePeriod} pricePercentChange={token.market?.pricePercentChange?.value}
/> timePeriod={timePeriod}
)} sparklineMap={props.sparklineMap}
/>
)
}
</ParentSize> </ParentSize>
</SparkLine> </SparkLine>
} }
......
...@@ -3,7 +3,7 @@ import { showFavoritesAtom } from 'components/Tokens/state' ...@@ -3,7 +3,7 @@ import { showFavoritesAtom } from 'components/Tokens/state'
import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens' import { PAGE_SIZE, useTopTokens } from 'graphql/data/TopTokens'
import { validateUrlChainParam } from 'graphql/data/util' import { validateUrlChainParam } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode, useCallback, useRef } from 'react' import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
...@@ -11,8 +11,6 @@ import styled from 'styled-components/macro' ...@@ -11,8 +11,6 @@ import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow' import { HeaderRow, LoadedRow, LoadingRow } from './TokenRow'
const LOADING_ROWS_COUNT = 3
const GridContainer = styled.div` const GridContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -58,13 +56,19 @@ function NoTokensState({ message }: { message: ReactNode }) { ...@@ -58,13 +56,19 @@ function NoTokensState({ message }: { message: ReactNode }) {
) )
} }
const LoadingRows = (rowCount?: number) => const LoadingRowsWrapper = styled.div`
Array(rowCount ?? PAGE_SIZE) margin-top: 8px;
.fill(null) `
.map((_, index) => {
return <LoadingRow key={index} /> const LoadingRows = (rowCount?: number) => (
}) <LoadingRowsWrapper>
const LoadingMoreRows = LoadingRows(LOADING_ROWS_COUNT) {Array(rowCount ?? PAGE_SIZE)
.fill(null)
.map((_, index) => {
return <LoadingRow key={index} />
})}
</LoadingRowsWrapper>
)
export function LoadingTokenTable({ rowCount }: { rowCount?: number }) { export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
return ( return (
...@@ -75,73 +79,51 @@ export function LoadingTokenTable({ rowCount }: { rowCount?: number }) { ...@@ -75,73 +79,51 @@ export function LoadingTokenTable({ rowCount }: { rowCount?: number }) {
) )
} }
export default function TokenTable() { export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) {
const showFavorites = useAtomValue<boolean>(showFavoritesAtom) 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 // 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 { error, loading, tokens, hasMore, loadMoreTokens, loadingRowCount } = useTopTokens(chainName) const { tokens, sparklines } = useTopTokens(chainName)
const showMoreLoadingRows = Boolean(loading && hasMore) setRowCount(tokens?.length ?? PAGE_SIZE)
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]
)
/* loading and error state */ /* loading and error state */
if (loading && (!tokens || tokens?.length === 0)) { if (!tokens) {
return <LoadingTokenTable rowCount={loadingRowCount} /> return (
<NoTokensState
message={
<>
<AlertTriangle size={16} />
<Trans>An error occurred loading tokens. Please try again.</Trans>
</>
}
/>
)
} else if (tokens?.length === 0) {
return showFavorites ? (
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} />
) : (
<NoTokensState message={<Trans>No tokens found</Trans>} />
)
} else { } else {
if (error || !tokens) { return (
return ( <GridContainer>
<NoTokensState <HeaderRow />
message={ <TokenDataContainer>
<> {tokens.map(
<AlertTriangle size={16} /> (token, index) =>
<Trans>An error occurred loading tokens. Please try again.</Trans> token && (
</> <LoadedRow
} key={token?.address}
/> tokenListIndex={index}
) tokenListLength={tokens.length}
} else if (tokens?.length === 0) { token={token}
return showFavorites ? ( sparklineMap={sparklines}
<NoTokensState message={<Trans>You have no favorited tokens</Trans>} /> />
) : ( )
<NoTokensState message={<Trans>No tokens found</Trans>} /> )}
) </TokenDataContainer>
} else { </GridContainer>
return ( )
<>
<GridContainer>
<HeaderRow />
<TokenDataContainer>
{tokens.map(
(token, index) =>
token && (
<LoadedRow
key={token?.address}
tokenListIndex={index}
tokenListLength={tokens.length}
token={token}
ref={index + 1 === tokens.length ? lastTokenRef : undefined}
/>
)
)}
{showMoreLoadingRows && LoadingMoreRows}
</TokenDataContainer>
</GridContainer>
</>
)
}
} }
} }
import { TokenSortMethod } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils' import { atomWithReset, atomWithStorage, useAtomValue } from 'jotai/utils'
import { useCallback, useMemo } from 'react' 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 favoritesAtom = atomWithStorage<string[]>('favorites', [])
export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false) export const showFavoritesAtom = atomWithStorage<boolean>('showFavorites', false)
export const filterStringAtom = atomWithReset<string>('') export const filterStringAtom = atomWithReset<string>('')
......
This diff is collapsed.
...@@ -42,7 +42,7 @@ import RemoveLiquidity from './RemoveLiquidity' ...@@ -42,7 +42,7 @@ import RemoveLiquidity from './RemoveLiquidity'
import RemoveLiquidityV3 from './RemoveLiquidity/V3' import RemoveLiquidityV3 from './RemoveLiquidity/V3'
import Swap from './Swap' import Swap from './Swap'
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects' import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
import Tokens, { LoadingTokens } from './Tokens' import Tokens from './Tokens'
const TokenDetails = lazy(() => import('./TokenDetails')) const TokenDetails = lazy(() => import('./TokenDetails'))
const Vote = lazy(() => import('./Vote')) const Vote = lazy(() => import('./Vote'))
...@@ -168,14 +168,7 @@ export default function App() { ...@@ -168,14 +168,7 @@ export default function App() {
<Routes> <Routes>
{tokensFlag === TokensVariant.Enabled && ( {tokensFlag === TokensVariant.Enabled && (
<> <>
<Route <Route path="tokens" element={<Tokens />}>
path="tokens"
element={
<Suspense fallback={<LoadingTokens />}>
<Tokens />
</Suspense>
}
>
<Route path=":chainName" /> <Route path=":chainName" />
</Route> </Route>
<Route <Route
......
...@@ -10,10 +10,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar' ...@@ -10,10 +10,11 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar'
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector' import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable' import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens' import { FavoriteTokensVariant, useFavoriteTokensFlag } from 'featureFlags/flags/favoriteTokens'
import { PAGE_SIZE } from 'graphql/data/TopTokens'
import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util' import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useResetAtom } from 'jotai/utils' import { useResetAtom } from 'jotai/utils'
import { useEffect } from 'react' import { Suspense, useEffect, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
...@@ -76,6 +77,8 @@ const Tokens = () => { ...@@ -76,6 +77,8 @@ const Tokens = () => {
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const connectedChainName = chainIdToBackendName(connectedChainId) const connectedChainName = chainIdToBackendName(connectedChainId)
const [rowCount, setRowCount] = useState(PAGE_SIZE)
useEffect(() => { useEffect(() => {
resetFilterString() resetFilterString()
}, [location, resetFilterString]) }, [location, resetFilterString])
...@@ -110,7 +113,9 @@ const Tokens = () => { ...@@ -110,7 +113,9 @@ const Tokens = () => {
<SearchBar /> <SearchBar />
</SearchContainer> </SearchContainer>
</FiltersWrapper> </FiltersWrapper>
<TokenTable /> <Suspense fallback={<LoadingTokenTable rowCount={rowCount} />}>
<TokenTable setRowCount={setRowCount} />
</Suspense>
</ExploreContainer> </ExploreContainer>
</Trace> </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