Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useEffect, useState } from 'react'

interface DeferredVisibilityOptions {
/**
* Expands the observed viewport so we hydrate content slightly before it
* scrolls into view.
*/
rootMargin?: string
/**
* When this key changes we reset the visibility state. Helpful when the same
* virtualized row instance renders different data.
*/
resetKey?: string | number
}

interface DeferredVisibilityResult<T extends HTMLElement> {
ref: (element: T | null) => void
isVisible: boolean
}

const DEFAULT_ROOT_MARGIN = '120px'

// Lightweight helper to delay hydration of expensive UI until the row is close to the viewport.
export function useDeferredVisibility<T extends HTMLElement>(
options: DeferredVisibilityOptions = {},
): DeferredVisibilityResult<T> {
const { rootMargin = DEFAULT_ROOT_MARGIN, resetKey } = options
const [element, setElement] = useState<T | null>(null)
const [isVisible, setIsVisible] = useState(false)

useEffect(() => {
if (resetKey === undefined) {
return
}

setIsVisible(false)
}, [resetKey])

useEffect(() => {
if (isVisible || !element) {
return undefined
}

if (typeof IntersectionObserver === 'undefined') {
setIsVisible(true)
return undefined
}

const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setIsVisible(true)
}
},
{ rootMargin },
)

observer.observe(element)

return () => observer.disconnect()
}, [element, isVisible, rootMargin])

const ref = useCallback((node: T | null) => {
setElement(node)
}, [])

return { ref, isVisible }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ export interface TokenInfoProps {
token: TokenWithLogo
className?: string
tags?: ReactNode
showAddress?: boolean
}

export function TokenInfo(props: TokenInfoProps): ReactNode {
const { token, className, tags } = props
const { token, className, tags, showAddress = true } = props

return (
<styledEl.Wrapper className={className}>
<TokenLogo token={token} sizeMobile={32} size={40} />
<styledEl.TokenDetails>
<styledEl.TokenSymbolWrapper>
<TokenSymbol token={token} />
<ClickableAddress address={token.address} chainId={token.chainId} />
{showAddress ? <ClickableAddress address={token.address} chainId={token.chainId} /> : null}
</styledEl.TokenSymbolWrapper>
<styledEl.TokenNameRow>
<TokenName token={token} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Nullish } from 'types'

import * as styledEl from './styled'

import { useDeferredVisibility } from '../../hooks/useDeferredVisibility'
import { TokenInfo } from '../TokenInfo'
import { TokenTags } from '../TokenTags'

Expand Down Expand Up @@ -58,6 +59,13 @@ export function TokenListItem(props: TokenListItemProps): ReactNode {
className,
} = props

const tokenKey = `${token.chainId}:${token.address.toLowerCase()}`
// Defer heavyweight UI (tooltips, formatted numbers) until the row is about to enter the viewport.
const { ref: visibilityRef, isVisible: hasIntersected } = useDeferredVisibility<HTMLDivElement>({
resetKey: tokenKey,
rootMargin: '200px',
})

const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
if (isTokenSelected) {
e.preventDefault()
Expand All @@ -74,11 +82,15 @@ export function TokenListItem(props: TokenListItemProps): ReactNode {
)

const isSupportedChain = token.chainId in SupportedChainId

const balanceAmount = balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined
const shouldShowBalances = isWalletConnected && isSupportedChain
// Formatting balances (BigNumber -> CurrencyAmount -> Fiat) is expensive; delay until the row is visible.
const shouldFormatBalances = shouldShowBalances && hasIntersected
const balanceAmount =
shouldFormatBalances && balance ? CurrencyAmount.fromRawAmount(token, balance.toHexString()) : undefined

return (
<styledEl.TokenItem
ref={visibilityRef}
data-address={token.address.toLowerCase()}
data-token-symbol={token.symbol || ''}
data-token-name={token.name || ''}
Expand All @@ -88,26 +100,56 @@ export function TokenListItem(props: TokenListItemProps): ReactNode {
>
<TokenInfo
token={token}
showAddress={hasIntersected}
tags={
<TokenTags
isUnsupported={isUnsupported}
isPermitCompatible={isPermitCompatible}
tags={token.tags}
tokenListTags={tokenListTags}
/>
hasIntersected ? (
<TokenTags
isUnsupported={isUnsupported}
isPermitCompatible={isPermitCompatible}
tags={token.tags}
tokenListTags={tokenListTags}
/>
) : null
}
/>
{isWalletConnected && (
<styledEl.TokenBalance>
{isSupportedChain ? (
<>
{balanceAmount ? <TokenAmount amount={balanceAmount} /> : LoadingElement}
{usdAmount ? <FiatAmount amount={usdAmount} /> : null}
</>
) : null}
</styledEl.TokenBalance>
)}
<TokenBalanceColumn
shouldShow={shouldShowBalances}
shouldFormat={shouldFormatBalances}
balanceAmount={balanceAmount}
usdAmount={usdAmount}
/>
{children}
</styledEl.TokenItem>
)
}

interface TokenBalanceColumnProps {
shouldShow: boolean
shouldFormat: boolean
balanceAmount?: CurrencyAmount<Currency>
usdAmount?: CurrencyAmount<Currency> | null
}

function TokenBalanceColumn({
shouldShow,
shouldFormat,
balanceAmount,
usdAmount,
}: TokenBalanceColumnProps): ReactNode {
if (!shouldShow) {
return null
}

return (
<styledEl.TokenBalance>
{shouldFormat ? (
<>
{balanceAmount ? <TokenAmount amount={balanceAmount} /> : LoadingElement}
{usdAmount ? <FiatAmount amount={usdAmount} /> : null}
</>
) : (
LoadingElement
)}
</styledEl.TokenBalance>
)
}