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" ;
8-
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- }
12+ let hydrated = false ;
1713
1814/**
1915 * This component will emulate the browser's scroll restoration on location
2016 * changes.
2117 *
2218 * @see https://remix.run/api/remix#scrollrestoration
2319 */
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- } , [ ] )
20+ export function ScrollRestoration ( {
21+ getKey,
22+ ...props
23+ } : ScriptProps & {
24+ getKey : ScrollRestorationPropsRR [ "getKey" ] ;
25+ } ) {
26+ let location = useLocation ( ) ;
27+ let matches = useMatches ( ) ;
28+
29+ useScrollRestoration ( {
30+ getKey,
31+ storageKey : STORAGE_KEY ,
32+ skip : ! hydrated ,
33+ } ) ;
34+
35+ // In order to support `getKey`, we need to compute a "key" here so we can
36+ // hydrate that up so that SSR scroll restoration isn't waiting on React to
37+ // hydrate. *However*, our key on the server is not the same as our key on
38+ // the client! So if the user's getKey implementation returns the SSR
39+ // location key, then let's ignore it and let our inline <script> below pick
40+ // up the client side history state key
41+ let key = React . useMemo (
42+ ( ) => {
43+ if ( ! getKey ) return null ;
44+ let userKey = getKey ( location , matches ) ;
45+ return userKey !== location . key ? userKey : null ;
46+ } ,
47+ // Nah, we only need this the first time for the SSR render
48+ // eslint-disable-next-line react-hooks/exhaustive-deps
49+ [ ]
3750 ) ;
3851
39- let restoreScroll = ( ( STORAGE_KEY : string ) => {
52+ let restoreScroll = ( ( STORAGE_KEY : string , restoreKey : string ) => {
4053 if ( ! window . history . state || ! window . history . state . key ) {
4154 let key = Math . random ( ) . toString ( 32 ) . slice ( 2 ) ;
4255 window . history . replaceState ( { key } , "" ) ;
4356 }
4457 try {
4558 let positions = JSON . parse ( sessionStorage . getItem ( STORAGE_KEY ) || "{}" ) ;
46- let storedY = positions [ window . history . state . key ] ;
59+ let storedY = positions [ restoreKey || window . history . state . key ] ;
4760 if ( typeof storedY === "number" ) {
4861 window . scrollTo ( 0 , storedY ) ;
4962 }
@@ -53,84 +66,21 @@ export function ScrollRestoration(props: ScriptProps) {
5366 }
5467 } ) . toString ( ) ;
5568
69+ // Use this to skip restoration on the initial load since we'll do it in
70+ // the inline <script> below
71+ if ( ! hydrated ) {
72+ hydrated = true ;
73+ }
74+
5675 return (
5776 < script
5877 { ...props }
5978 suppressHydrationWarning
6079 dangerouslySetInnerHTML = { {
61- __html : `(${ restoreScroll } )(${ JSON . stringify ( STORAGE_KEY ) } )` ,
80+ __html : `(${ restoreScroll } )(${ JSON . stringify (
81+ STORAGE_KEY
82+ ) } , ${ JSON . stringify ( key ) } )`,
6283 } }
6384 />
6485 ) ;
6586}
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