feat: add scroll_restoration component#57
Open
Archmonger wants to merge 7 commits into
Open
Conversation
Archmonger
force-pushed
the
fix-31-scroll-restoration
branch
from
July 21, 2026 14:56
3b0c921 to
57e4294
Compare
Introduces a component that saves and restores scroll positions across client-side navigation. - JS side: ScrollRestoration component that intercepts pushState/ replaceState and popstate events to save scroll positions keyed by pathname. Restores scroll via requestAnimationFrame after each render. Disables browser's native scrollRestoration on mount. - Python side: scroll_restoration(*children) component that wraps children in a scroll-aware div and renders the hidden JS component. Exported from reactpy_router top-level. - Tests: basic rendering test and scroll preservation test that scrolls, navigates away, and verifies restoration on return. - Docs: added scroll_restoration to reference/components.md. - Changelog: updated Unreleased section.
- Use module-level _scrollPositions dict instead of useRef so saved scroll positions persist across route transitions (component remounts). - Restore scroll on mount (useEffect[] with cleanup) rather than on every render, preventing scroll jumps on re-renders. - Consume (delete) scroll position after restoration so it fires once per navigation cycle.
Archmonger
force-pushed
the
fix-31-scroll-restoration
branch
from
July 21, 2026 15:02
57e4294 to
b08cabe
Compare
Archmonger
marked this pull request as ready for review
July 21, 2026 15:10
Preact may perform multiple render commits during a single navigation event (server VDOM -> layout effects -> client-side effects). Each commit can reset scroll position. Fix by retrying scrollTo each animation frame (up to 5 retries) until the position sticks.
The ScrollRestoration component is reused by Preact across route navigations (same component type at the same VDOM position). The mount effect (with [] deps) only fires once, so the restoration logic must run in a post-render effect (no deps) to detect pathname changes. Key changes: - Post-render effect restores scroll on every render if a saved position exists for the current pathname - Position is kept alive (not deleted) so Preact render commits during navigation don't lose it - Retry across rAF frames (up to 10) to survive Preact's multi-commit navigation cycle - Python: use 'min-height: 100vh' instead of 'height: 100%' to not constrain document height
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a
scroll_restorationcomponent that saves and restores scroll positions across client-side navigation, analogous to React Router'sScrollRestorationcomponent.The component:
<div>pushState/replaceStateto save scroll before navigationpopstate(browser back/forward) as wellrequestAnimationFrameafter each renderscrollRestorationon mount to prevent interferenceCloses #31
Checklist
By submitting this pull request I agree that all contributions comply with this project's open source license(s).