Skip to content

Commit 917b75e

Browse files
authored
ScrollRestoration on RR 6.4 (#4844)
1 parent afd6aa2 commit 917b75e

File tree

3 files changed

+41
-122
lines changed

3 files changed

+41
-122
lines changed

packages/remix-react/components.tsx

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -864,29 +864,6 @@ function dedupe(array: any[]) {
864864
return [...new Set(array)];
865865
}
866866

867-
/**
868-
* Setup a callback to be fired on the window's `beforeunload` event. This is
869-
* useful for saving some data to `window.localStorage` just before the page
870-
* refreshes, which automatically happens on the next `<Link>` click when Remix
871-
* detects a new version of the app is available on the server.
872-
*
873-
* Note: The `callback` argument should be a function created with
874-
* `React.useCallback()`.
875-
*
876-
* @see https://remix.run/api/remix#usebeforeunload
877-
*/
878-
export function useBeforeUnload(
879-
callback: (event: BeforeUnloadEvent) => any
880-
): void {
881-
// TODO: Export from react-router-dom
882-
React.useEffect(() => {
883-
window.addEventListener("beforeunload", callback);
884-
return () => {
885-
window.removeEventListener("beforeunload", callback);
886-
};
887-
}, [callback]);
888-
}
889-
890867
// TODO: Can this be re-exported from RR?
891868
export interface RouteMatch {
892869
/**

packages/remix-react/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type {
1212
export {
1313
Form,
1414
Outlet,
15+
useBeforeUnload,
1516
useFormAction,
1617
useHref,
1718
useLocation,
@@ -48,7 +49,6 @@ export {
4849
useLoaderData,
4950
useMatches,
5051
useActionData,
51-
useBeforeUnload,
5252
} from "./components";
5353

5454
export type { FormMethod, FormEncType } from "./data";
Lines changed: 40 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,60 @@
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";
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

Comments
 (0)