11import * as React from "react" ;
2- import { useLocation } from "react-router-dom" ;
2+ import type { ScrollRestorationProps as ScrollRestorationPropsRR } from "react-router-dom" ;
3+ import {
4+ useLocation ,
5+ UNSAFE_useScrollRestoration as useScrollRestoration ,
6+ } from "react-router-dom" ;
37
4- import { useBeforeUnload , useTransition } from "./components" ;
58import type { ScriptProps } from "./components" ;
9+ import { useMatches } from "./components" ;
610
711let STORAGE_KEY = "positions" ;
812
9- let positions : { [ key : string ] : number } = { } ;
10-
11- if ( typeof document !== "undefined" ) {
12- let sessionPositions = sessionStorage . getItem ( STORAGE_KEY ) ;
13- if ( sessionPositions ) {
14- positions = JSON . parse ( sessionPositions ) ;
15- }
16- }
17-
1813/**
1914 * This component will emulate the browser's scroll restoration on location
2015 * changes.
2116 *
2217 * @see https://remix.run/api/remix#scrollrestoration
2318 */
24- export function ScrollRestoration ( props : ScriptProps ) {
25- useScrollRestoration ( ) ;
26-
27- // wait for the browser to restore it on its own
28- React . useEffect ( ( ) => {
29- window . history . scrollRestoration = "manual" ;
30- } , [ ] ) ;
31-
32- // let the browser restore on it's own for refresh
33- useBeforeUnload (
34- React . useCallback ( ( ) => {
35- window . history . scrollRestoration = "auto" ;
36- } , [ ] )
19+ export function ScrollRestoration ( {
20+ getKey,
21+ ...props
22+ } : ScriptProps & {
23+ getKey : ScrollRestorationPropsRR [ "getKey" ] ;
24+ } ) {
25+ let location = useLocation ( ) ;
26+ let matches = useMatches ( ) ;
27+
28+ useScrollRestoration ( {
29+ getKey,
30+ storageKey : STORAGE_KEY ,
31+ } ) ;
32+
33+ // In order to support `getKey`, we need to compute a "key" here so we can
34+ // hydrate that up so that SSR scroll restoration isn't waiting on React to
35+ // hydrate. *However*, our key on the server is not the same as our key on
36+ // the client! So if the user's getKey implementation returns the SSR
37+ // location key, then let's ignore it and let our inline <script> below pick
38+ // up the client side history state key
39+ let key = React . useMemo (
40+ ( ) => {
41+ if ( ! getKey ) return null ;
42+ let userKey = getKey ( location , matches ) ;
43+ return userKey !== location . key ? userKey : null ;
44+ } ,
45+ // Nah, we only need this the first time for the SSR render
46+ // eslint-disable-next-line react-hooks/exhaustive-deps
47+ [ ]
3748 ) ;
3849
39- let restoreScroll = ( ( STORAGE_KEY : string ) => {
50+ let restoreScroll = ( ( STORAGE_KEY : string , restoreKey : string ) => {
4051 if ( ! window . history . state || ! window . history . state . key ) {
4152 let key = Math . random ( ) . toString ( 32 ) . slice ( 2 ) ;
4253 window . history . replaceState ( { key } , "" ) ;
4354 }
4455 try {
4556 let positions = JSON . parse ( sessionStorage . getItem ( STORAGE_KEY ) || "{}" ) ;
46- let storedY = positions [ window . history . state . key ] ;
57+ let storedY = positions [ restoreKey || window . history . state . key ] ;
4758 if ( typeof storedY === "number" ) {
4859 window . scrollTo ( 0 , storedY ) ;
4960 }
@@ -58,79 +69,10 @@ export function ScrollRestoration(props: ScriptProps) {
5869 { ...props }
5970 suppressHydrationWarning
6071 dangerouslySetInnerHTML = { {
61- __html : `(${ restoreScroll } )(${ JSON . stringify ( STORAGE_KEY ) } )` ,
72+ __html : `(${ restoreScroll } )(${ JSON . stringify (
73+ STORAGE_KEY
74+ ) } , ${ JSON . stringify ( key ) } )`,
6275 } }
6376 />
6477 ) ;
6578}
66-
67- let hydrated = false ;
68-
69- function useScrollRestoration ( ) {
70- let location = useLocation ( ) ;
71- let transition = useTransition ( ) ;
72-
73- let wasSubmissionRef = React . useRef ( false ) ;
74-
75- React . useEffect ( ( ) => {
76- if ( transition . submission ) {
77- wasSubmissionRef . current = true ;
78- }
79- } , [ transition ] ) ;
80-
81- React . useEffect ( ( ) => {
82- if ( transition . location ) {
83- positions [ location . key ] = window . scrollY ;
84- }
85- } , [ transition , location ] ) ;
86-
87- useBeforeUnload (
88- React . useCallback ( ( ) => {
89- sessionStorage . setItem ( STORAGE_KEY , JSON . stringify ( positions ) ) ;
90- } , [ ] )
91- ) ;
92-
93- if ( typeof document !== "undefined" ) {
94- // eslint-disable-next-line
95- React . useLayoutEffect ( ( ) => {
96- // don't do anything on hydration, the component already did this with an
97- // inline script.
98- if ( ! hydrated ) {
99- hydrated = true ;
100- return ;
101- }
102-
103- let y = positions [ location . key ] ;
104-
105- // been here before, scroll to it
106- if ( y != undefined ) {
107- window . scrollTo ( 0 , y ) ;
108- return ;
109- }
110-
111- // try to scroll to the hash
112- if ( location . hash ) {
113- let el = document . getElementById ( location . hash . slice ( 1 ) ) ;
114- if ( el ) {
115- el . scrollIntoView ( ) ;
116- return ;
117- }
118- }
119-
120- // don't do anything on submissions
121- if ( wasSubmissionRef . current === true ) {
122- wasSubmissionRef . current = false ;
123- return ;
124- }
125-
126- // otherwise go to the top on new locations
127- window . scrollTo ( 0 , 0 ) ;
128- } , [ location ] ) ;
129- }
130-
131- React . useEffect ( ( ) => {
132- if ( transition . submission ) {
133- wasSubmissionRef . current = true ;
134- }
135- } , [ transition ] ) ;
136- }
0 commit comments