fix(feed): preserve scroll when returning from posts - #6677
Conversation
Wait for feed height changes instead of abandoning scroll restoration after two seconds. Preserve each modal history entry's originating feed position so reopening and closing posts through browser history does not reset it to zero. Cover delayed rendering, user cancellation, viewport changes, history traversal, remounts, and cancelled navigation.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
rebelchris
left a comment
There was a problem hiding this comment.
Summary
Two mechanisms are changed here: useScrollRestoration swaps a 2s rAF budget for a ResizeObserver, and usePostModalNavigation starts keying the originating feed offset by history entry. The history-key keying is the right idea and the modal/back traces hold up. Two things I'd want resolved before merge, plus some consolidation asks.
Context I'd flag up front: this is the third pass at the same symptom (#6409, then the 1s → 2s budget now being deleted). Per the PR description no browser or device testing was done, and the tests mock window.scrollTo, ResizeObserver, the router and timers — so nothing in the evidence here demonstrates the fix on the runtime where it fails (a mid-range phone on a cache-restored feed). Could you confirm which device/preview was actually checked, and what the before/after looked like? Every previous iteration of this fix was correct in the diff and wrong in the runtime.
Blocking
usePostModalNavigation.ts—onCloseModalrestores with an unguardedwindow.scrollTo(inline).useScrollRestoration.ts— the restoration attempt is now unbounded in time (inline).
Non-blocking
- Two parallel scroll-position stores and two history-key readers (inline).
new ResizeObserverwithout the repo'stypeof ResizeObserver === 'undefined'guard (inline).- Unrelated strict-typing edits inside a fix PR (inline).
Reviewed by AI.
Route modal close through the same height-aware restoration helper and save its new feed history entry. Bound attempts to ten seconds, cancel on native scrolling, and fall back to bounded polling without ResizeObserver. Consolidate history lookup and cap saved positions. Remove unrelated typing changes requested in review. Add clamping-aware hook regressions and native Chromium checks with Pixel 5 emulation and CPU throttling.
rebelchris
left a comment
There was a problem hiding this comment.
Re-review of 163387c
Both blockers are properly resolved, and the shape is better than what I suggested:
lib/scrollRestoration.tsis now a single height-aware restoration used by both the hook and modal close, so the close path no longer scrolls blind. Saving the feed offset under the destination entry and lettinguseScrollRestorationpick it up is the right seam.- The 10s bound is back, with a
popstate/native-scroll cancel and a regression test asserting nothing restores after the deadline. ResizeObserveris guarded with a bounded polling fallback, matching the rest of the repo.- The unrelated typing changes are gone, and the store is capped with LRU eviction.
One new finding on the added e2e, plus the verification ask still open. No blockers from me on the hook code itself.
Reviewed by AI.
| compilerOptions: { | ||
| module: ts.ModuleKind.CommonJS, | ||
| target: ts.ScriptTarget.ES2020, | ||
| }, |
There was a problem hiding this comment.
Non-blocking, but I'd drop this file: this reads scrollRestoration.ts off disk, transpiles it at runtime with the typescript compiler API and injects the output as a script tag into a synthetic <div id="feed" style="height:2000px"> fixture. Nothing else in packages/playwright works this way, and it is a pattern that will quietly rot — a future refactor that splits the module, adds an import, or changes the export shape breaks the harness rather than the feature.
More importantly, it does not test what the PR fixes. There is no feed, no post modal, no usePostModalNavigation, no back navigation — it asserts that restoreScrollPosition scrolls to 5000 once a div grows, which the Jest suite already covers with fake timers. The CPU throttling and Pixel 5 project make it look like device verification, but the page under test is a two-element fixture, so it proves nothing about the real feed on a real phone.
Suggested direction: either drop it and keep the Jest coverage, or write it against the actual app (baseURL, open a feed, open a post, close it, assert scrollY) in ./tests so it runs with the existing config.
Reviewed by AI.
| import { defineConfig, devices } from '@playwright/test'; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: './regressions', |
There was a problem hiding this comment.
Non-blocking: a second Playwright config with its own testDir: './regressions' means this suite is outside playwright.config.ts (testDir: './tests'), so neither pnpm test nor the e2e-tests workflow will ever run it — it only runs if somebody remembers pnpm test:scroll-restoration. A regression test nobody runs is not a regression test. If the suite survives the comment above, please fold it into the existing config as a project rather than adding a parallel one.
Reviewed by AI.
|
|
||
| scrollPositionOnFeed.current = 0; | ||
| if (navigated && feedScrollPosition !== undefined) { | ||
| saveScrollPosition(baseAsPath, feedScrollPosition); |
There was a problem hiding this comment.
Nit, no action needed if intentional: on the webapp this both saves the position and starts a restoration, while the asPath change makes useScrollRestoration's effect find the same value and start a second one (the first is cancelled by cancelScrollRestoration inside). Harmless, and I assume the explicit call is here because the extension's router does not drive that effect — worth a line in the description so the redundancy is not read as a bug later.
Small related asymmetry: the key is written from ${webappUrl}posts/${postId} and read back from router.asPath. They agree today because the push sets as to exactly that string, but webappUrl concatenation has bitten this repo before; deriving both sides from one expression would be safer.
Reviewed by AI.
|
Correction on my part, and a red check. That is my fault: I asked for the strict-typing edits to be split out as unrelated scope, but The substantive review stands: the two blockers are fixed, the e2e harness comments are still open. Reviewed by AI. |
Changes
Readers could return from a post to the top of the feed when cached content took longer than two seconds to render, or when closing a post reopened through browser Back.
Events
No new tracking events.
Experiment
No new experiments.
Browser Testing
Ran ten local fixture checks using Chromium 145.0.7632.6: desktop and Pixel 5 emulation, both with 4x CPU throttling. These execute the production restoration helper with real document layout, ResizeObserver, timers, scroll events, and browser history. They do not mock scrolling or layout.
These browser checks were run during development; their temporary Playwright suite, configuration, and package command are not included in this PR.
No physical-device test was performed. The PR preview was opened, but its home route redirected this browser session to onboarding, so the authenticated application feed has not been verified. The browser results above are local fixtures, not claims about a physical phone or the preview's cached feed.
Validation
node ./scripts/typecheck-strict-changed.js. All 13 modal navigation errors are fixed without suppressions.Preview domain
https://codex-fix-feed-scroll-restoratio.preview.app.daily.dev