Skip to content

feat(tokenselector): 2 - add useDeferredVisibility hook#6519

Merged
fairlighteth merged 27 commits intodevelopfrom
feat/token-selector-2
Dec 4, 2025
Merged

feat(tokenselector): 2 - add useDeferredVisibility hook#6519
fairlighteth merged 27 commits intodevelopfrom
feat/token-selector-2

Conversation

@fairlighteth
Copy link
Copy Markdown
Contributor

@fairlighteth fairlighteth commented Nov 13, 2025

Summary

This PR delivers the token-row performance foundations from the split plan: a shared useDeferredVisibility hook plus deferred rendering for token tags/address and balance/fiat formatting so the selector only hydrates rows that are near the viewport.

To Test

  1. Token selector scrolling

    • Open the token selector on a long network and scroll top-to-bottom; rows should populate smoothly without the earlier jank.
    • Scroll quickly—tags and addresses should appear just before the row enters view, never staying blank once visible.
    • Watch the balance column: a loading shimmer should show off-screen, then switch to formatted balances/fiat once visible.
  2. Wallet disconnected

    • Disconnect the wallet and confirm the balance column remains hidden (same as before).
  3. Re-filtering

    • Switch chains or update the search query; previously rendered rows should reset their deferred state and only hydrate again after they re-enter the viewport.

Background

Part of the staged token-selector rollout, focusing on deferring expensive row work (logos, tags, fiat) until necessary.

Summary by CodeRabbit

Release Notes

  • New Features
    • Optimized token list rendering: token details and balances now load efficiently as items approach the viewport instead of all at once.
    • Added optional address visibility control in token information displays for enhanced UI customization.

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
cowfi Ready Ready Preview Dec 4, 2025 10:37am
explorer-dev Ready Ready Preview Dec 4, 2025 10:37am
swap-dev Ready Ready Preview Dec 4, 2025 10:37am
widget-configurator Ready Ready Preview Dec 4, 2025 10:37am
2 Skipped Deployments
Project Deployment Preview Updated (UTC)
cosmos Ignored Ignored Dec 4, 2025 10:37am
sdk-tools Ignored Ignored Preview Dec 4, 2025 10:37am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 13, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

A new useDeferredVisibility hook delays rendering of expensive UI elements until they approach the viewport using IntersectionObserver. The hook is integrated into TokenListItem to conditionally render address, tags, and balance formatting based on visibility state, while a showAddress prop is added to TokenInfo for conditional address display.

Changes

Cohort / File(s) Change Summary
Deferred Visibility Hook
apps/cowswap-frontend/src/modules/tokensList/hooks/useDeferredVisibility.ts
New React hook that uses IntersectionObserver to delay rendering of UI elements until they near the viewport; accepts rootMargin (default "120px") and resetKey options; returns ref and visibility state.
Token Info Component
apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx
Added optional showAddress prop (default true) to conditionally render ClickableAddress; previously always rendered.
Token List Item Optimization
apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
Integrated useDeferredVisibility hook to defer rendering of address, tags, and balance formatting until intersection; added TokenBalanceColumn internal component to conditionally render formatted balance and fiat value based on visibility state.

Sequence Diagram

sequenceDiagram
    participant Component as TokenListItem
    participant Hook as useDeferredVisibility
    participant Observer as IntersectionObserver
    
    Component->>Hook: Mount with ref & options
    Hook->>Hook: Initialize state (isVisible=false)
    
    alt IntersectionObserver Available
        Hook->>Observer: Create observer with rootMargin
        Observer->>Observer: Monitor element visibility
        Observer-->>Hook: Element intersects viewport
        Hook->>Hook: Set isVisible=true
    else SSR/Unsupported
        Hook->>Hook: Immediately set isVisible=true
    end
    
    Hook-->>Component: Return ref + isVisible state
    Component->>Component: Conditionally render:<br/>- ClickableAddress<br/>- TokenTags<br/>- TokenBalanceColumn
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • IntersectionObserver integration: Verify proper observer creation, cleanup, and SSR fallback behavior
  • Conditional rendering logic: Ensure shouldShowBalances and shouldFormatBalances flags correctly control UI rendering in TokenListItem
  • TokenBalanceColumn component: Review new internal component encapsulation and props flow
  • resetKey behavior: Confirm that visibility state properly resets when the key changes

Possibly related PRs

Suggested reviewers

  • shoom3301
  • elena-zh
  • cowdan

Poem

🐰 A hook that watches and waits with care,
Rendering tokens when they're almost there,
IntersectionObserver's gentle gaze,
Defers the heavy work through the viewport's maze,
Performance hops forward with UI so light!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(tokenselector): 2 - add useDeferredVisibility hook' directly describes the main change: adding a new useDeferredVisibility hook for token selector performance optimization.
Description check ✅ Passed The PR description includes all required template sections: Summary explaining the purpose, a comprehensive 'To Test' section with detailed testing steps and checkboxes, and a Background section providing context.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alfetopito
Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 18, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx (1)

62-67: Excellent integration of deferred visibility.

The tokenKey construction at line 62 provides a stable identity for each unique token by combining chainId and the lowercased address. This ensures the resetKey properly triggers re-hydration when the same DOM element is reused to render different tokens in virtualized scenarios.

The hook usage with rootMargin: '200px' is appropriate for pre-loading content just before it enters the viewport, balancing performance with user experience.

Optional suggestion: Consider extracting the hardcoded '200px' to a named constant (e.g., TOKEN_ROW_INTERSECTION_MARGIN) for easier maintenance and consistency if this value is used elsewhere or might need tuning.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae654a and f06936c.

📒 Files selected for processing (3)
  • apps/cowswap-frontend/src/modules/tokensList/hooks/useDeferredVisibility.ts (1 hunks)
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx (1 hunks)
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-08-08T13:55:17.528Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/state/tokens/allTokensAtom.ts:78-78
Timestamp: 2025-08-08T13:55:17.528Z
Learning: In libs/tokens/src/state/tokens/allTokensAtom.ts (TypeScript/Jotai), the team prefers to wait for token lists to initialize (listsStatesListAtom non-empty) before returning tokens. No fallback to favorites/user-added/native tokens should be used when listsStatesList is empty.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
📚 Learning: 2025-08-08T13:56:18.009Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6125
File: libs/tokens/src/updaters/TokensListsUpdater/index.tsx:29-31
Timestamp: 2025-08-08T13:56:18.009Z
Learning: In libs/tokens/src/updaters/TokensListsUpdater/index.tsx, the project’s current Jotai version requires using `unstable_getOnInit` (not `getOnInit`) in atomWithStorage options; keep `{ unstable_getOnInit: true }` until Jotai is upgraded.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx
📚 Learning: 2025-07-24T16:42:53.154Z
Learnt from: cowdan
Repo: cowprotocol/cowswap PR: 6009
File: apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/HighFeeWarningTooltipContent.tsx:23-33
Timestamp: 2025-07-24T16:42:53.154Z
Learning: In apps/cowswap-frontend/src/modules/tradeWidgetAddons/containers/HighFeeWarning/HighFeeWarningTooltipContent.tsx, the use of toFixed(2) for percentage formatting in tooltip content is intentional and differs from the banner message formatting that uses toSignificant(2, undefined, Rounding.ROUND_DOWN). This formatting difference serves different UX purposes and should not be flagged as inconsistent.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
📚 Learning: 2025-08-12T06:33:19.348Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6137
File: libs/tokens/src/state/tokens/allTokensAtom.ts:34-65
Timestamp: 2025-08-12T06:33:19.348Z
Learning: In libs/tokens/src/utils/parseTokenInfo.ts, the parseTokenInfo() function returns a new instance of TokenInfo using object spread syntax ({ ...token, ... }), making it safe to mutate properties like lpTokenProvider on the returned object without side effects.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx
📚 Learning: 2025-10-10T20:28:16.565Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6347
File: apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx:49-49
Timestamp: 2025-10-10T20:28:16.565Z
Learning: In apps/cowswap-frontend/src/modules/trade, TradeConfirmation follows a two-layer architecture: TradeConfirmationView (pure/stateless) in pure/TradeConfirmation/index.tsx renders the UI, while TradeConfirmation (container) in containers/TradeConfirmation/index.tsx wraps it to freeze props during pending trades (via useStableTradeConfirmationProps), wire in signing state (useSigningStep), and inject trade confirmation state (useTradeConfirmState). Consuming modules should import the container TradeConfirmation from 'modules/trade' to preserve this stateful behavior.
<!-- [add_learning]
When reviewing component refactoring in apps/cowswap-frontend/src/modules/trade, recognize the pattern where a pure view component (e.g., TradeConfirmationView) is separated from a stateful container (e.g., TradeConfirmation) that wraps it. The container adds runtime state management (prop stabilization, signing state, etc.) while the view remains testable and composable. Do not flag usages that import th...

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx
📚 Learning: 2025-08-12T06:33:19.348Z
Learnt from: shoom3301
Repo: cowprotocol/cowswap PR: 6137
File: libs/tokens/src/state/tokens/allTokensAtom.ts:34-65
Timestamp: 2025-08-12T06:33:19.348Z
Learning: In libs/tokens/src/state/tokens/allTokensAtom.ts, the parseTokenInfo() function returns a new instance of TokenInfo each time, making it safe to mutate properties like lpTokenProvider on the returned object without side effects.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx
📚 Learning: 2025-09-19T11:38:59.206Z
Learnt from: fairlighteth
Repo: cowprotocol/cowswap PR: 6232
File: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx:199-200
Timestamp: 2025-09-19T11:38:59.206Z
Learning: The makeBuildClickEvent function in apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx takes five parameters: defaultChainId, contextLabel, mode, isSwapMode, and chainsCount. The chainsCount parameter is used to determine the CrossChain flag in analytics events.

Applied to files:

  • apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx
🔇 Additional comments (5)
apps/cowswap-frontend/src/modules/tokensList/pure/TokenInfo/index.tsx (1)

15-27: LGTM! Clean conditional rendering with backward compatibility.

The addition of the optional showAddress prop with a default value of true ensures existing usage remains unaffected while enabling the deferred rendering strategy. The conditional rendering at line 27 is straightforward and correct.

apps/cowswap-frontend/src/modules/tokensList/hooks/useDeferredVisibility.ts (1)

24-68: Solid implementation with correct one-time hydration pattern.

The hook correctly implements deferred visibility using IntersectionObserver:

  • The effect dependencies [element, isVisible, rootMargin] are accurate.
  • The early return at line 40 when isVisible is true prevents re-observation, implementing the intended one-time hydration behavior.
  • The resetKey effect (lines 31-37) properly resets visibility when token data changes, enabling correct re-hydration in virtualized list scenarios.
  • The ref callback (lines 63-65) is stable with empty dependencies, avoiding unnecessary re-renders.
  • Fallback to immediate visibility when IntersectionObserver is unavailable handles SSR and older browsers gracefully.
  • The cleanup function at line 60 ensures the observer is properly disconnected, preventing memory leaks.

Note: If rootMargin were passed dynamically (e.g., inline string on each render), it would cause the observer to be recreated unnecessarily. However, the current usage in TokenListItem (line 66) uses a static '200px' string, so this is not a concern in practice.

apps/cowswap-frontend/src/modules/tokensList/pure/TokenListItem/index.tsx (3)

85-89: Well-structured deferred balance formatting.

The two-level gating is correct:

  • shouldShowBalances ensures balances are only shown when the wallet is connected and the chain is supported.
  • shouldFormatBalances defers the expensive CurrencyAmount.fromRawAmount conversion until after the row has intersected the viewport.

This approach successfully delays the computational cost of balance formatting until it's actually needed.


101-113: Correct conditional rendering based on intersection.

Both the showAddress prop (line 103) and the TokenTags rendering (lines 105-112) are correctly gated by hasIntersected, ensuring these UI elements are only hydrated once the row is near the viewport. This aligns with the performance goals stated in the PR objectives.


126-155: TokenBalanceColumn correctly implements the three-state balance UI.

The component properly handles:

  1. Hidden state (!shouldShow): Returns null when wallet is disconnected or chain is unsupported.
  2. Loading state (shouldShow && !shouldFormat): Shows LoadingElement before the row intersects the viewport.
  3. Formatted state (shouldFormat): Displays formatted balance and fiat amount after intersection.

The fallback to LoadingElement at line 147 when balanceAmount is undefined (balance not yet fetched) is reasonable and preserves the original behavior while adding deferred rendering.

className,
} = props

const tokenKey = `${token.chainId}:${token.address.toLowerCase()}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: not a very big deal for this pr, but we need to unify methods like comparing addresses and getting id in helpers due to solana/other networks integrations (f.e. in solana the upper and lower case matters). It will be better to define method like getTokenId(token): string

jfyi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a follow up PR (11) -> #6542

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants