Skip to content

Commit

Permalink
ScrollRestoration on RR 6.4 (#4844)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Dec 16, 2022
1 parent afd6aa2 commit 917b75e
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 122 deletions.
23 changes: 0 additions & 23 deletions packages/remix-react/components.tsx
Expand Up @@ -864,29 +864,6 @@ function dedupe(array: any[]) {
return [...new Set(array)];
}

/**
* Setup a callback to be fired on the window's `beforeunload` event. This is
* useful for saving some data to `window.localStorage` just before the page
* refreshes, which automatically happens on the next `<Link>` click when Remix
* detects a new version of the app is available on the server.
*
* Note: The `callback` argument should be a function created with
* `React.useCallback()`.
*
* @see https://remix.run/api/remix#usebeforeunload
*/
export function useBeforeUnload(
callback: (event: BeforeUnloadEvent) => any
): void {
// TODO: Export from react-router-dom
React.useEffect(() => {
window.addEventListener("beforeunload", callback);
return () => {
window.removeEventListener("beforeunload", callback);
};
}, [callback]);
}

// TODO: Can this be re-exported from RR?
export interface RouteMatch {
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-react/index.tsx
Expand Up @@ -12,6 +12,7 @@ export type {
export {
Form,
Outlet,
useBeforeUnload,
useFormAction,
useHref,
useLocation,
Expand Down Expand Up @@ -48,7 +49,6 @@ export {
useLoaderData,
useMatches,
useActionData,
useBeforeUnload,
} from "./components";

export type { FormMethod, FormEncType } from "./data";
Expand Down
138 changes: 40 additions & 98 deletions packages/remix-react/scroll-restoration.tsx
@@ -1,49 +1,60 @@
import * as React from "react";
import { useLocation } from "react-router-dom";
import type { ScrollRestorationProps as ScrollRestorationPropsRR } from "react-router-dom";
import {
useLocation,
UNSAFE_useScrollRestoration as useScrollRestoration,
} from "react-router-dom";

import { useBeforeUnload, useTransition } from "./components";
import type { ScriptProps } from "./components";
import { useMatches } from "./components";

let STORAGE_KEY = "positions";

let positions: { [key: string]: number } = {};

if (typeof document !== "undefined") {
let sessionPositions = sessionStorage.getItem(STORAGE_KEY);
if (sessionPositions) {
positions = JSON.parse(sessionPositions);
}
}

/**
* This component will emulate the browser's scroll restoration on location
* changes.
*
* @see https://remix.run/api/remix#scrollrestoration
*/
export function ScrollRestoration(props: ScriptProps) {
useScrollRestoration();

// wait for the browser to restore it on its own
React.useEffect(() => {
window.history.scrollRestoration = "manual";
}, []);

// let the browser restore on it's own for refresh
useBeforeUnload(
React.useCallback(() => {
window.history.scrollRestoration = "auto";
}, [])
export function ScrollRestoration({
getKey,
...props
}: ScriptProps & {
getKey: ScrollRestorationPropsRR["getKey"];
}) {
let location = useLocation();
let matches = useMatches();

useScrollRestoration({
getKey,
storageKey: STORAGE_KEY,
});

// In order to support `getKey`, we need to compute a "key" here so we can
// hydrate that up so that SSR scroll restoration isn't waiting on React to
// hydrate. *However*, our key on the server is not the same as our key on
// the client! So if the user's getKey implementation returns the SSR
// location key, then let's ignore it and let our inline <script> below pick
// up the client side history state key
let key = React.useMemo(
() => {
if (!getKey) return null;
let userKey = getKey(location, matches);
return userKey !== location.key ? userKey : null;
},
// Nah, we only need this the first time for the SSR render
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

let restoreScroll = ((STORAGE_KEY: string) => {
let restoreScroll = ((STORAGE_KEY: string, restoreKey: string) => {
if (!window.history.state || !window.history.state.key) {
let key = Math.random().toString(32).slice(2);
window.history.replaceState({ key }, "");
}
try {
let positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "{}");
let storedY = positions[window.history.state.key];
let storedY = positions[restoreKey || window.history.state.key];
if (typeof storedY === "number") {
window.scrollTo(0, storedY);
}
Expand All @@ -58,79 +69,10 @@ export function ScrollRestoration(props: ScriptProps) {
{...props}
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `(${restoreScroll})(${JSON.stringify(STORAGE_KEY)})`,
__html: `(${restoreScroll})(${JSON.stringify(
STORAGE_KEY
)}, ${JSON.stringify(key)})`,
}}
/>
);
}

let hydrated = false;

function useScrollRestoration() {
let location = useLocation();
let transition = useTransition();

let wasSubmissionRef = React.useRef(false);

React.useEffect(() => {
if (transition.submission) {
wasSubmissionRef.current = true;
}
}, [transition]);

React.useEffect(() => {
if (transition.location) {
positions[location.key] = window.scrollY;
}
}, [transition, location]);

useBeforeUnload(
React.useCallback(() => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
}, [])
);

if (typeof document !== "undefined") {
// eslint-disable-next-line
React.useLayoutEffect(() => {
// don't do anything on hydration, the component already did this with an
// inline script.
if (!hydrated) {
hydrated = true;
return;
}

let y = positions[location.key];

// been here before, scroll to it
if (y != undefined) {
window.scrollTo(0, y);
return;
}

// try to scroll to the hash
if (location.hash) {
let el = document.getElementById(location.hash.slice(1));
if (el) {
el.scrollIntoView();
return;
}
}

// don't do anything on submissions
if (wasSubmissionRef.current === true) {
wasSubmissionRef.current = false;
return;
}

// otherwise go to the top on new locations
window.scrollTo(0, 0);
}, [location]);
}

React.useEffect(() => {
if (transition.submission) {
wasSubmissionRef.current = true;
}
}, [transition]);
}

0 comments on commit 917b75e

Please sign in to comment.