Skip to content

Commit 5659a46

Browse files
committed
ScrollRestoration on RR 6.4
1 parent 5d472de commit 5659a46

File tree

1 file changed

+48
-98
lines changed

1 file changed

+48
-98
lines changed
Lines changed: 48 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,62 @@
11
import * 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";
58
import type { ScriptProps } from "./components";
9+
import { useMatches } from "./components";
610

711
let 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

Comments
 (0)