Skip to content

feat: add scroll_restoration component#57

Open
Archmonger wants to merge 7 commits into
mainfrom
fix-31-scroll-restoration
Open

feat: add scroll_restoration component#57
Archmonger wants to merge 7 commits into
mainfrom
fix-31-scroll-restoration

Conversation

@Archmonger

Copy link
Copy Markdown
Contributor

Description

Adds a scroll_restoration component that saves and restores scroll positions across client-side navigation, analogous to React Router's ScrollRestoration component.

The component:

  • Wraps server-side children in a scroll-aware <div>
  • Renders a hidden JavaScript component that manages scroll positions keyed by URL pathname
  • Patches pushState/replaceState to save scroll before navigation
  • Saves scroll on popstate (browser back/forward) as well
  • Restores scroll via requestAnimationFrame after each render
  • Disables the browser's native scrollRestoration on mount to prevent interference

Closes #31

Checklist

  • Tests have been developed for bug fixes or new functionality.
  • The changelog has been updated, if necessary.
  • Documentation has been updated, if necessary.
  • GitHub Issues closed by this PR have been linked.

By submitting this pull request I agree that all contributions comply with this project's open source license(s).

@Archmonger
Archmonger force-pushed the fix-31-scroll-restoration branch from 3b0c921 to 57e4294 Compare July 21, 2026 14:56
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
Archmonger force-pushed the fix-31-scroll-restoration branch from 57e4294 to b08cabe Compare July 21, 2026 15:02
@Archmonger
Archmonger marked this pull request as ready for review July 21, 2026 15:10
@Archmonger
Archmonger requested a review from a team as a code owner 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
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.

scroll_restoration component

1 participant