Skip to content

Add fixture: trimming a capped chat from the front breaks maintainScrollAtEnd#462

Open
frankberliner wants to merge 1 commit into
LegendApp:mainfrom
frankberliner:add-chat-trim-fixture
Open

Add fixture: trimming a capped chat from the front breaks maintainScrollAtEnd#462
frankberliner wants to merge 1 commit into
LegendApp:mainfrom
frankberliner:add-chat-trim-fixture

Conversation

@frankberliner

Copy link
Copy Markdown

Hi Jay — following up from our chat, here's the minimal repro as a fixture in the example app. 🙏

Use case

A Kick.com (Twitch-style) livestream chat client. Very high message throughput, and the app runs ~12h/day, so we cap the message list to a fixed window (e.g. 500) by dropping the oldest messages from the front of the data array.

The behavior we want

Standard live chat: pinned to the bottom, newest at the bottom, new messages push older ones up; auto-stick when at the bottom; pause + show a "scroll to latest" button when the user scrolls up. That part works great with alignItemsAtEnd + maintainScrollAtEnd + maintainVisibleContentPosition.

The bug

Once the list starts trimming from the front (capping), being pinned at the bottom breaks:

  • maintainVisibleContentPosition={{ data: false }} → the viewport visibly jumps on every trim (removed top content isn't compensated).
  • data: true → the removal is compensated (no jump), but maintainScrollAtEnd stops following: after the anchor adjustment isWithinMaintainScrollAtEndThreshold flips to false, so the newest messages stay just below the fold (a "freeze").

Append-only works flawlessly — it's specifically the front-trim + maintain-at-end interaction.

Repro (this fixture)

Fixtures → Chat & Keyboard → Chat Trim (capped). It auto-appends fake messages and caps at MAX = 40. Watch latest #N in the header vs the number on the bottom message: they match while append-only, then diverge the moment trimming starts. Flip maintainVisibleContentPosition data: true ↔ false in the fixture to see freeze vs jump.

Workarounds we tried (chunk/hysteresis trimming so it's infrequent + a forced scrollToEnd right after each trim) reduce but don't fully eliminate it. Any guidance or a supported pattern for a fixed-size sliding window (trim from the top while staying pinned to the bottom) would be hugely appreciated. Thanks for the great library!

🤖 Generated with Claude Code

…-trim

A high-throughput chat that caps its message list by removing the oldest
items from the front of the data array breaks maintainScrollAtEnd once
trimming starts: with maintainVisibleContentPosition data:true the list
stops following the newest message (freeze), and with data:false the
viewport jumps on each trim. Append-only works perfectly until the cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88b340b931

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

setMessages((prev) => {
// Append a small batch to simulate throughput / batched updates.
const batchSize = 1 + (idCounter % 3); // 1..3 per tick
const next = [...prev, ...Array.from({ length: batchSize }, makeMessage)];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep message generation out of the state updater

When this fixture is run in a React dev tree that replays state updater functions (for example StrictMode/concurrent development), this updater can be invoked more than once for a single committed tick, but makeMessage mutates the module-level idCounter. That can advance latest #N and even change the batch contents for discarded updater calls, so the header can diverge from the bottom row for reasons unrelated to the maintainScrollAtEnd regression the fixture is meant to demonstrate. Generate the next messages from a ref/local counter in an effect before calling setMessages, or otherwise keep the updater pure.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant