@@ -119,37 +119,49 @@ export function Navigate({
119119 return null ;
120120}
121121
122+ // Module-level scroll positions keyed by pathname. Shared across all
123+ // ScrollRestoration component instances so saved positions survive
124+ // unmount/remount during route transitions.
125+ const _scrollPositions : Record < string , { x : number ; y : number } > = { } ;
126+
122127/**
123128 * ScrollRestoration component that saves and restores scroll positions across
124129 * client-side navigation. Uses the browser's History API to track scroll
125130 * positions keyed by URL pathname.
126131 *
127- * On mount, it disables the browser's default scroll restoration and takes
128- * over the responsibility of saving and restoring scroll positions.
129- * Scroll positions are saved when:
130- * - The user navigates via pushState/replaceState (Link or Navigate component)
131- * - The user triggers a popstate event (browser back/forward)
132+ * On mount, it:
133+ * - Disables the browser's native scroll restoration
134+ * - Patches pushState/replaceState to save scroll positions before navigation
135+ * - Listens for popstate to save the leaving page's scroll
136+ * - Restores any previously saved scroll position for the current pathname
132137 *
133- * Scroll positions are restored after each render when the user returns to
134- * a previously visited page via history navigation.
138+ * Scroll positions are consumed on restore so subsequent re-renders on the same
139+ * page do not snap the user back. The module-level store survives the
140+ * component's unmount/remount cycle across route transitions.
135141 */
136142export function ScrollRestoration ( { } : ScrollRestorationProps ) : null {
137- // Store scroll positions keyed by pathname
138- const positions = React . useRef < Record < string , { x : number ; y : number } > > ( { } ) ;
139- // Track the last known pathname for popstate handling
140143 const lastPathRef = React . useRef ( window . location . pathname ) ;
141144
142- // On mount, disable the browser's built-in scroll restoration so we can
143- // manage scroll positions ourselves. Also patch pushState and replaceState
144- // to save the scroll position of the page being navigated away from.
145145 React . useEffect ( ( ) => {
146+ // Restore any saved scroll position for the current pathname on mount.
147+ // The component mounts/unmounts on route changes, so this runs once
148+ // per navigation. The position is consumed after restoration.
149+ const currentKey = window . location . pathname ;
150+ const savedPos = _scrollPositions [ currentKey ] ;
151+ if ( savedPos ) {
152+ delete _scrollPositions [ currentKey ] ;
153+ requestAnimationFrame ( ( ) => {
154+ window . scrollTo ( savedPos . x , savedPos . y ) ;
155+ } ) ;
156+ }
157+
146158 window . history . scrollRestoration = "manual" ;
147159
148160 // Patch pushState to save scroll before URL changes
149161 const originalPushState = window . history . pushState . bind ( window . history ) ;
150162 window . history . pushState = ( data , unused , url ) => {
151163 const key = window . location . pathname ;
152- positions . current [ key ] = { x : window . scrollX , y : window . scrollY } ;
164+ _scrollPositions [ key ] = { x : window . scrollX , y : window . scrollY } ;
153165 originalPushState ( data , unused , url ) ;
154166 lastPathRef . current = window . location . pathname ;
155167 } ;
@@ -158,17 +170,17 @@ export function ScrollRestoration({}: ScrollRestorationProps): null {
158170 const originalReplaceState = window . history . replaceState . bind ( window . history ) ;
159171 window . history . replaceState = ( data , unused , url ) => {
160172 const key = window . location . pathname ;
161- positions . current [ key ] = { x : window . scrollX , y : window . scrollY } ;
173+ _scrollPositions [ key ] = { x : window . scrollX , y : window . scrollY } ;
162174 originalReplaceState ( data , unused , url ) ;
163175 lastPathRef . current = window . location . pathname ;
164176 } ;
165177
166- // On popstate (browser back/forward), save the scroll of the page being
167- // left and restore the scroll of the page being returned to.
178+ // On popstate, save the scroll of the page we're leaving.
179+ // The popstate event fires before the next render, so we use
180+ // lastPathRef (the pathname before navigation) as the key.
168181 const handlePopState = ( ) => {
169- // Save scroll for the page we're leaving
170182 const leavingPath = lastPathRef . current ;
171- positions . current [ leavingPath ] = { x : window . scrollX , y : window . scrollY } ;
183+ _scrollPositions [ leavingPath ] = { x : window . scrollX , y : window . scrollY } ;
172184 lastPathRef . current = window . location . pathname ;
173185 } ;
174186
@@ -181,17 +193,5 @@ export function ScrollRestoration({}: ScrollRestorationProps): null {
181193 } ;
182194 } , [ ] ) ;
183195
184- // After every render, restore scroll for the current URL if we have a
185- // saved position. Uses requestAnimationFrame to let the DOM settle first.
186- React . useEffect ( ( ) => {
187- const key = window . location . pathname ;
188- const pos = positions . current [ key ] ;
189- if ( pos ) {
190- requestAnimationFrame ( ( ) => {
191- window . scrollTo ( pos . x , pos . y ) ;
192- } ) ;
193- }
194- } ) ;
195-
196196 return null ;
197197}
0 commit comments