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

1 participant