Skip to content

[Web] Smooth out lineup infinite-scroll on Trending and Feed#14286

Open
raymondjacobson wants to merge 5 commits intomainfrom
claude/serene-wilson-e53f41
Open

[Web] Smooth out lineup infinite-scroll on Trending and Feed#14286
raymondjacobson wants to merge 5 commits intomainfrom
claude/serene-wilson-e53f41

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

Web port of the recent mobile fix in apps#14272. The tanquery-driven TrackLineup (Trending, Feed, Profile Tracks/Reposts, Search Tracks, Listening History, Remixes, Contest Submissions) felt rough at the bottom of a page: scrolling fast, you'd hit the literal end of the list with nothing visible, then late skeletons would appear, then tracks. Two things stacked:

  • Fixed 500px threshold in react-infinite-scroller. On tall monitors the user could blow past it before the next page even started loading.
  • Skeletons gated on tanquery's isFetching — they only painted after the scroll handler → loadNextPagefetchNextPage → tanquery state round-trip. That's a multi-tick gap after the user already reached the bottom.

This PR:

  • Sizes the threshold to ~one viewport (the scroll parent's clientHeight, tracked with a ResizeObserver), matching mobile's onEndReachedThreshold = 1. Falls back to 800px until the parent is measured.
  • Adds a local isLoadMoreTriggered flag set the same tick the scroll handler fires, so skeletons render on the very next frame instead of waiting for isFetching to flip. Cleared when entries arrive or the parent finishes fetching.
  • Routes InfiniteScroll's loadMore through a single handleLoadMore guarded by the flag, so we never double-trigger.

Single load-more entry point now:

const handleLoadMore = useCallback(() => {
  if (!hasNextPage || isFetching || isLoadMoreTriggered) return
  if (!loadNextPage) return
  setIsLoadMoreTriggered(true)
  loadNextPage()
}, [hasNextPage, isFetching, isLoadMoreTriggered, loadNextPage])

Skeleton render conditions consult both the parent state and the local trigger:

{(isFetching || isLoadMoreTriggered) && tiles.length > 0
  ? renderSkeletons(Math.min(maxEntries - tiles.length, pageSize))
  : null}

Test plan

  • Open Trending (Week / Month / All-Time) on desktop, scroll fast — skeletons appear before reaching the bottom and the next page loads in.
  • Same on the Feed page.
  • Mobile-web: scroll Trending / Feed in a phone-sized viewport (useWindow path) — same smooth behavior.
  • Resize the browser between page loads — threshold updates via ResizeObserver.
  • Verify other TrackLineup consumers still paginate correctly (Profile Tracks / Reposts, Search Tracks, Track remixes, Listening History, Contest Submissions).
  • Verify the empty state still renders when a tab has no tracks.

🤖 Generated with Claude Code

Mirror the recent mobile fix (#14272) on the web TrackLineup. Two issues
stacked at the bottom of tanquery-driven lineups:

- The `react-infinite-scroller` threshold was a fixed 500px, so on tall
  monitors the user could blow past it before paint.
- Skeletons rendered only once tanquery's `isFetching` flipped to true —
  a multi-tick round-trip after the scroll handler fires, long enough
  for the user to land on the literal bottom of the list with nothing
  there.

This change:

- Sizes the threshold to ~one viewport (clientHeight of the scroll
  parent, observed via ResizeObserver), matching mobile's
  `onEndReachedThreshold = 1`.
- Adds a local `isLoadMoreTriggered` flag set the same tick the scroll
  handler fires, so skeletons render on the very next frame instead of
  waiting for `isFetching` to propagate. Cleared once new entries
  arrive or the parent finishes fetching.
- Routes InfiniteScroll's `loadMore` through a single `handleLoadMore`
  guarded by the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 9, 2026

🦋 Changeset detected

Latest commit: 9899823

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@audius/web Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

🌐 Web preview ready

Preview URL: https://audius-web-preview-pr-14286.audius.workers.dev

Unique preview for this PR (deployed from this branch).
Workflow run

raymondjacobson and others added 2 commits May 8, 2026 18:13
The first pass at smoothing infinite-scroll wasn't enough on desktop
because two more things stacked on top of the late-skeleton problem:

- A 1-viewport threshold isn't enough buffer for fast desktop scrolls
  (mouse fling, trackpad, PgDn). Bumped to 2 viewports so the next page
  request fires while there's still meaningful content below.
- The skeleton count was `pageSize`, which is only 4 on Trending / Feed.
  At ~124px per desktop tile that's ~480px of skeletons — half a
  viewport — so the user could blow right through them and land on the
  literal bottom of the list while waiting for the network. Skeleton
  count is now `max(pageSize, ceil(threshold / approxTileHeight))` so
  the loading window always fills the threshold area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the smoother infinite-scroll work, both visible while
scrolling Trending:

1. Skeleton tiles in ordered lineups now reserve the order column. The
   desktop TrackTile previously hid the rank number and crown when
   `isLoading`, so the order column collapsed to ~0 width on a skeleton
   and the artwork shifted left when real data arrived. With the
   skeleton now rendering the index it's been told (`tiles.length + i`),
   the number / crown stay visible and the layout doesn't jump on load.

2. Skeleton tail is now constant while `hasNextPage`. Before, skeletons
   were gated on `isFetching || isLoadMoreTriggered`, so the page
   ballooned by ~one threshold's worth of skeletons when fetching, then
   shrank back when the page resolved. On a fast scroll deep into that
   shrinking region, the browser had to clamp `scrollTop`, which felt
   like the page "bouncing" mid-scroll. Keeping a constant skeleton
   tail until the end of the lineup means the scroll height only ever
   grows (smoothly, by `pageSize` tile-heights per resolved page) until
   `hasNextPage` flips to false at end-of-lineup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pull-request-size pull-request-size Bot added size/L and removed size/M labels May 9, 2026
The shared default (`TRENDING_LOAD_MORE_PAGE_SIZE` /
`FEED_LOAD_MORE_PAGE_SIZE = 4`) is tuned for mobile viewports and slower
touch scrolling. On desktop a fast trackpad / wheel scroll covers ~2
viewports per second, but each 4-track page only adds ~480px of real
content — so successive load-mores can't keep up and the lineup
visibly lags behind the user even with the synchronous skeleton tail.

Override at the desktop call sites only, leaving the shared constant
(and mobile's behavior) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant