人们如何使用 react-router v4 处理滚动恢复?

IT技术 reactjs react-router
2021-04-08 01:15:44

使用 react-router 时,我在后退按钮(历史弹出状态)上的滚动位置遇到了一些问题。React router v4 不处理开箱即用的滚动管理,因为浏览器正在实现一些自动滚动行为。这很好,除非浏览器窗口的高度从一个视图到另一个视图变化太大。我已经实现了 ScrollToTop 组件,如下所述:https : //reacttraining.com/react-router/web/guides/scroll-restoration

这很好用。当您单击链接并转到不同的组件时,浏览器会滚动到顶部(就像普通的服务器呈现的网站一样)。只有当您返回(通过浏览器后退按钮)到具有更高窗口高度的视图时才会出现此问题。似乎 (chrome) 试图在 react 呈现内容(和浏览器高度)之前转到上一页的滚动位置。这导致滚动只会根据它来自的视图的高度尽可能向下移动。想象一下这个场景:

View1:一长串电影(窗口高度 3500px)。
(电影被点击)
View2 : 所选电影的详细视图(窗口高度:1000px)。
(点击浏览器后退按钮)
返回视图 1,但滚动位置不能超过 1000px,因为 chrome 试图在 react 渲染长电影列表之前设置位置。

出于某种原因,这只是 Chrome 中的一个问题。Firefox 和 Safari 似乎处理得很好。我想知道其他人是否遇到过这个问题,以及你们通常如何在 React 中处理滚动恢复。

注意:所有电影都是从 sampleMovies.js 导入的——所以在我的示例中我不会等待 API 响应。

6个回答

请注意,这history.scrollRestoration只是一种禁用浏览器自动尝试滚动恢复的方法,这通常不适用于单页应用程序,因此它们不会干扰应用程序想要执行的任何操作。除了切换到手动滚动恢复之外,您还需要某种库来提供浏览器的历史 API、React 的渲染以及窗口的滚动位置和任何可滚动块元素之间的集成。

在无法为 React Router 4 找到这样的滚动恢复库之后,我创建了一个名为react-scroll-manager 的库它支持在导航到新位置时滚动到顶部(又名历史推送)和向后/向前滚动恢复(又名历史弹出)。除了滚动窗口之外,它还可以滚动您包装在 ElementScroller 组件中的任何嵌套元素。它还通过使用MutationObserver在用户指定的时间限制内观看窗口/元素内容来支持延迟/异步渲染这种延迟渲染支持适用于滚动恢复以及使用哈希链接滚动到特定元素。

npm install react-scroll-manager

import React from 'react';
import { Router } from 'react-router-dom';
import { ScrollManager, WindowScroller, ElementScroller } from 'react-scroll-manager';
import { createBrowserHistory as createHistory } from 'history';

class App extends React.Component {
  constructor() {
    super();
    this.history = createHistory();
  }
  render() {
    return (
      <ScrollManager history={this.history}>
        <Router history={this.history}>
          <WindowScroller>
            <ElementScroller scrollKey="nav">
              <div className="nav">
                ...
              </div>
            </ElementScroller>
            <div className="content">
              ...
            </div>
          </WindowScroller>
        </Router>
      </ScrollManager>
    );
  }
}

请注意,需要 HTML5 浏览器(IE 10+)和 React 16。HTML5 提供了历史 API,该库使用了 React 16 中的现代 Context 和 Ref API。

请看一下这个问题:stackoverflow.com/questions/58772954/...谢谢!
2021-05-25 01:15:44
嘿,我只想说我今天找到了你的图书馆,而且效果很好!谢谢你!
2021-06-07 01:15:44
您还应该在滚动到顶部的同时重置键盘焦点。我写了一个东西来照顾它:github.com/oaf-project/oaf-react-router
2021-06-08 01:15:44
@Trevor Robinson,您能否确认您的解决方案适用于 React Router v5?我已经按照您的描述尝试了 ElementScroller 和 WindowScroller,但它根本不起作用。没有错误。
2021-06-10 01:15:44
我确实让它部分地与 React Router v4 一起工作。导航链接点击滚动到顶部的新页面,但不保留历史中的滚动位置。
2021-06-16 01:15:44

你如何处理你的滚动恢复?

原来浏览器有history.scrollRestoration 的实现。

也许你可以使用它?检查这些链接。

https://developer.mozilla.org/en/docs/Web/API/History#Specifications https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration

此外,我发现了一个 npm module,它可能能够轻松处理滚动恢复,但该库仅适用于 react router v3 及以下版本

https://www.npmjs.com/package/react-router-restore-scroll https://github.com/ryanflorence/react-router-restore-scroll

我希望这会有所帮助。

如 react-scroll-manager 文档中所述,history.scrollRestoration 在浏览器之间不一致。我发现它在 Chrome 中根本不起作用。
2021-05-29 01:15:44

我最终使用 localStorage 来跟踪滚动位置 - 不确定这是否可以处理所有情况。

在此示例中,有一个带有一组 Stores 的 Company 页面,每个 Store 都有一组 Display case。我需要跟踪展示柜的滚动位置,因此将其保存到“storeScrollTop”键。需要添加 6 行代码。

公司.jsx:

// click on a store link
const handleClickStore = (evt) => {
  window.localStorage.removeItem('storeScrollTop') // <-- reset scroll value
  const storeId = evt.currentTarget.id
  history.push(`/store/${storeId}`)
}

商店.jsx:

// initialize store page
React.useEffect(() => {
  // fetch displays
  getStoreDisplays(storeId).then(objs => setObjs(objs)).then(() => {
    // get the 'store' localstorage scrollvalue and scroll to it
    const scrollTop = Number(window.localStorage.getItem('storeScrollTop') || '0')
    const el = document.getElementsByClassName('contents')[0]
    el.scrollTop = scrollTop
  })
}, [storeId])

// click on a display link    
const handleClickDisplay = (evt) => {
  // save the scroll pos for return visit
  const el = document.getElementsByClassName('contents')[0]
  window.localStorage.setItem('storeScrollTop', String(el.scrollTop))
  // goto the display
  const displayId = evt.currentTarget.id
  history.push(`/display/${displayId}`)
}

最棘手的部分是找出哪个元素具有正确的 scrollTop 值 - 我必须在控制台中检查内容,直到找到它为止。

我的解决方案:使用 Map (ES6) 为每个路径名保存 window.scrollY

滚动管理器.tsx

import { FC, useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function debounce(fn: (...params: any) => void, wait: number): (...params: any) => void {
  let timer: any = null;
  return function(...params: any){
    clearTimeout(timer);
    timer = setTimeout(()=>{
      fn(...params)
    }, wait);
  }
}

export const pathMap = new Map<string, number>();

const Index: FC = () => {
  const { pathname } = useLocation();

  useEffect(() => {
    if (pathMap.has(pathname)) {
      window.scrollTo(0, pathMap.get(pathname)!)
    } else {
      pathMap.set(pathname, 0);
      window.scrollTo(0, 0);
    }
  }, [pathname]);

  useEffect(() => {
    const fn = debounce(() => {
      pathMap.set(pathname, window.scrollY);
    }, 200);

    window.addEventListener('scroll', fn);
    return () => window.removeEventListener('scroll', fn);
  }, [pathname]);

  return null;
};

export default Index;

应用程序.tsx

function App() {

  return (
      <Router>
        <ScrollManager/>
        <Switch>...</Switch>
      </Router>
  );
}

也可以pathMap.size === 1用来判断用户是否是第一次进入应用

如果页面是新的,但如果页面在恢复滚动之前看到,则此组件向上滚动。

滚动到顶部.tsx 文件:

import { useEffect, FC } from 'react';
import { useLocation } from 'react-router-dom';

const pathNameHistory = new Set<string>();

const Index: FC = (): null => {
  const { pathname } = useLocation();

  useEffect((): void => {
    if (!pathNameHistory.has(pathname)) {
      window.scrollTo(0, 0);
      pathNameHistory.add(pathname);
    }
  }, [pathname]);

  return null;
};

export default Index;

app.tsx 文件:

<BrowserRouter>
  <ScrollToTop />
  <App />
</BrowserRouter>