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

时间:2017-07-07 12:01:11

标签: reactjs react-router

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

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

查看1:长电影列表(窗口高度3500px) (点击电影)
View2 :所选影片的详情视图(窗口高度:1000px) (单击浏览器后退按钮)
返回查看1 ,但滚动位置不能超过1000px,因为Chrome会在反应呈现长影片列表之前设置位置。

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

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

5 个答案:

答案 0 :(得分:3)

请注意,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。

答案 1 :(得分:2)

你如何处理你的卷轴恢复?

结果是浏览器具有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模块可能能够轻松地处理滚动恢复,但是这个库只适用于反应路由器v3及以下

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

我希望这可以提供帮助。

答案 2 :(得分:0)

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

在此示例中,有一个“公司”页面,其中包含一组商店,每个商店都有一组展示柜。我需要跟踪展示柜的滚动位置,因此将其保存到“ storeScrollTop”键中。有6行代码要添加。

company.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}`)
}

store.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值-我必须在控制台中进行检查,直到找到它。

答案 3 :(得分:0)

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

scroll-to-top.tsx文件:

SELECT
campaign, 
sub_event,
quantity
FROM Purchasing p
WHERE sub_event = (SELECT p3.sub_event
                    FROM Purchasing p3
                    WHERE p3.campaign = p.campaign
                    AND p3.sub_event IS NOT NULL
                    ORDER BY field(p3.sub_event, 'Completed', 'Recorded', 'Delivered')
                    LIMIT 1
                   );

app.tsx文件:

correlated subqueries

答案 4 :(得分:0)

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

scroll-manager.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;

App.tsx

function App() {

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

您还可以使用 pathMap.size === 1 来确定用户是否是第一次进入应用程序