Add fixture: trimming a capped chat from the front breaks maintainScrollAtEnd#462
Add fixture: trimming a capped chat from the front breaks maintainScrollAtEnd#462frankberliner wants to merge 1 commit into
Conversation
…-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>
There was a problem hiding this comment.
💡 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)]; |
There was a problem hiding this comment.
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 👍 / 👎.
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), butmaintainScrollAtEndstops following: after the anchor adjustmentisWithinMaintainScrollAtEndThresholdflips tofalse, 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. Watchlatest #Nin the header vs the number on the bottom message: they match while append-only, then diverge the moment trimming starts. FlipmaintainVisibleContentPositiondata: true ↔ falsein the fixture to see freeze vs jump.Workarounds we tried (chunk/hysteresis trimming so it's infrequent + a forced
scrollToEndright 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