使用 React Router 6 导航时恢复滚动位置

Restore scroll position when navigating with React Router 6

如何设置 React Router 6 以在导航和刷新浏览器 window 时恢复滚动位置?

React Router 5 有一个 page about scroll restoration, but I can't find anything about scrolling in the docs for v6,所以我猜你应该自己用另一个包来处理这个问题。很公平,但我找不到任何与 React Router 6 兼容的东西。

react-scroll-restoration and oaf-react-router require v5. (oaf-react-router does list that it supports v6, but the basic usage code example isn't compatible with v6, and the related issue #210 仍然打开。)

Gatsby 和 Next.js 支持开箱即用的滚动恢复,但我看起来没有一个你可以直接使用的打包好的包。

这个小 demo app with server side rendered pages 可以满足我的要求。来回导航和刷新浏览器时滚动位置恢复 window。

这里是the same app using React Router 6,滚动位置没有保存和恢复,而是在页面之间实际重复使用。通常的解决方法是在导航页面时滚动到顶部,但我对这种行为不感兴趣。

编辑:React Query writes that the issue with scroll restoration is that pages are refetching data,这意味着如果用于呈现页面的数据存在,滚动恢复就可以正常工作。我无法确认这一点,因为我的小型 React Router 6 应用程序即使根本没有进行任何数据获取也有问题。我觉得为了让它工作,我觉得我缺少一些小东西。

Rant:我很惊讶这个问题的典型答案是在导航时调用 window.scrollTo(0, 0)。这仅解决了滚动位置在页面之间传输的问题。当滚动位置未恢复时,在页面之间导航时的用户体验会严重恶化。我想这就是弹出窗口 windows 变得如此流行的部分原因,但它们会带来一长串其他用户体验问题,所以我真的想避免使用它们。

感谢 this comment in oaf-react-router 我能够让它与 React Router 6 一起工作。不过有一些注意事项,所以我认为这不是专业网络应用程序的可行解决方案。

  1. this code comment 所述,oaf-react-router 必须使用与 react-router-dom 相同的 history 版本。这就是 HistoryRouter 导出为 unstable_HistoryRouter 的原因。这个方案确实感觉挺不稳定的

  2. oaf-react-router刷新网页时不恢复滚动位置。我不知道这是否可以轻松实现,并且这是可以接受的。

import { createBrowserHistory } from 'history';
import { wrapHistory } from 'oaf-react-router';
import React from 'react';
import ReactDOM from 'react-dom';
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';

const history = createBrowserHistory();
wrapHistory(history);

ReactDOM.render(
  <React.StrictMode>
    <HistoryRouter history={history}>
      <App />
    </HistoryRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

Here a full working example on StackBlitz.

我知道我已经发布了一个答案,但我最终得到了一个不同的解决方案,我认为它值得一个单独的答案。

我首先要意识到的是StackBlitz and CodeSandbox both have some custom route handling that breaks scroll restoration。在单独 window 中打开演示应用程序时,这个问题仍然存在。如果应用程序在正常环境中 运行,则在使用浏览器的后退和前进按钮在页面之间导航时会恢复滚动位置。

这两个问题仍然存在:

  1. 在应用程序中使用 links 导航时,页面之间的滚动位置保持不变。从技术上讲,当使用 React Router 的 <Link to="..."/>navigate(...) 时。页面应该滚动到顶部。

    回顾:转到页面 A,向下滚动并单击带你到页面 B 的 link。如果你使用浏览器的后退按钮导航回 A,滚动位置应该恢复。如果您在页面 B 上使用 link 返回 A,则滚动位置应位于页面顶部。

  2. 刷新页面时应恢复(保留)页面的滚动位置。刷新时页面滚动到顶部

我使用下面的代码解决了第一个问题,并承认第二个问题仍然存在。我认为 Gatsby 和 Next.js 能够通过将滚动位置存储在会话存储中来解决刷新问题,这对我的用例来说有点过分了。

import { MouseEventHandler, ReactNode } from "react";
import { Link } from "react-router-dom";
import { useNavigateToTop } from "./useNavigateToTop";

interface Props {
  children: ReactNode;
  className?: string;
  to: string;
}

/** Link to the top of a page so that the scroll position isn't persisted between pages. Use this instead of React's build-in @see {@link Link}. */
export const LinkToTop = (props: Props) => {
  const navigateToTop = useNavigateToTop();

  const navigateAndReset: MouseEventHandler<HTMLAnchorElement> = (event) => {
    event.preventDefault();
    navigateToTop(props.to);
  };

  return (
    <Link className={props.className} onClick={navigateAndReset} to={props.to}>
      {props.children}
    </Link>
  );
};

import { useNavigate } from "react-router-dom";

/** Navigate to the top of a page so that the scroll position isn't persisted between pages. Use this instead of React Dom's build-in @see {@link useNavigate}. */
export const useNavigateToTop = () => {
  const navigate = useNavigate();

  const navigateAndReset = (to: string) => {
    navigate(to, { replace: true });
    window.scrollTo(0, 0);
  };

  return navigateAndReset;
};

使用 createBrowserHistory 并将 yscroll 存储在会话存储中,可以实现这一点。 参考下面link: https://medium.com/@knl.shivam.gupta/react-hooks-scroll-position-using-react-router-dom-v6-6cd59730b18d

这里,v6中的location.state用于将yscroll存储在浏览器历史栈中,从而可以使用useLayoutEffect来操作显示。