React Router v4 - 切换组件时保持滚动位置

Posted

技术标签:

【中文标题】React Router v4 - 切换组件时保持滚动位置【英文标题】:React Router v4 - Keep scrolling position when switching components 【发布时间】:2019-01-25 03:56:59 【问题描述】:

我有两个用 react-router 创建的 <Route>s。

/cards -> 纸牌游戏列表 /cards/1 -> 纸牌游戏 #1 的详细信息

当用户点击“返回列表”时,我想将用户滚动到他在列表中的位置。

我该怎么做?

【问题讨论】:

【参考方案1】:

工作示例codesandbox

React Router v4 不提供对滚动恢复的开箱即用支持,目前也不支持。在他们文档的React Router V4 - Scroll Restoration 部分中,您可以阅读更多相关信息。

因此,由每个开发人员编写逻辑来支持这一点,但我们确实有一些工具可以实现这一点。

element.scrollIntoView()

.scrollIntoView() 可以在一个元素上调用,你可以猜到,它会将它滚动到视图中。支持相当不错,目前97%的浏览器都支持。 Source: icanuse

<Link /> 组件可以传递状态

React Router 的 Link 组件有一个 to 属性,你可以提供一个对象而不是字符串。这就是它的样子。

<Link to= pathname: '/card', state: 9 >Card nine</Link>

我们可以使用状态将信息传递给将要渲染的组件。在这个例子中,state 被分配了一个数字,它足以回答你的问题,你稍后会看到,但它可以是任何东西。 /card 渲染 &lt;Card /&gt; 的路由现在可以访问 props.location.state 的变量状态,我们可以随意使用它。

标记每个列表项

在渲染各种卡片时,我们为每张卡片添加一个唯一的类。这样我们就有了一个可以传递的标识符,并且当我们导航回卡片列表概览时,我们知道需要将该项目滚动到视图中。

解决方案

    &lt;Cards /&gt; 呈现一个列表,每个项目都有一个唯一的类; 当一个项目被点击时,Link /&gt; 将唯一标识符传递给&lt;Card /&gt;&lt;Card /&gt; 呈现卡片详细信息和带有唯一标识符的后退按钮; 单击按钮并安装&lt;Cards /&gt; 时,.scrollIntoView() 使用来自props.location.state 的数据滚动到先前单击的项目。

下面是各个部分的一些代码sn-ps。

// Cards component displaying the list of available cards.
// Link's to prop is passed an object where state is set to the unique id.
class Cards extends React.Component 
  componentDidMount() 
    const item = document.querySelector(
      ".restore-" + this.props.location.state
    );
    if (item) 
      item.scrollIntoView();
    
  

  render() 
    const cardKeys = Object.keys(cardData);
    return (
      <ul className="scroll-list">
        cardKeys.map(id => 
          return (
            <Link
              to= pathname: `/cards/$id`, state: id 
              className=`card-wrapper restore-$id`
            >
              cardData[id].name
            </Link>
          );
        )
      </ul>
    );
  


// Card compoment. Link compoment passes state back to cards compoment
const Card = props => 
  const  id  = props.match.params;
  return (
    <div className="card-details">
      <h2>cardData[id].name</h2>
      <img alt=cardData[id].name src=cardData[id].image />
      <p>
        cardData[id].description&nbsp;<a href=cardData[id].url>More...</a>
      </p>
      <Link
        to=
          pathname: "/cards",
          state: props.location.state
        
      >
        <button>Return to list</button>
      </Link>
    </div>
  );
;

// App router compoment.
function App() 
  return (
    <div className="App">
      <Router>
        <div>
          <Route exact path="/cards" component=Cards />
          <Route path="/cards/:id" component=Card />
        </div>
      </Router>
    </div>
  );


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

【讨论】:

这回答了问题,但没有解决用户点击浏览器后退按钮时的问题【参考方案2】:

由于没有关于如何在功能组件中执行此操作的答案,这是我为一个项目实现的挂钩解决方案:

import React from 'react';
import  useHistory  from 'react-router-dom';

function useScrollMemory(): void 
  const history = useHistory< scroll: number  | undefined>();

  React.useEffect(() => 
    const  push, replace  = history;

    // Override the history PUSH method to automatically set scroll state.
    history.push = (path: string) => 
      push(path,  scroll: window.scrollY );
    ;
    // Override the history REPLACE method to automatically set scroll state.
    history.replace = (path: string) => 
      replace(path,  scroll: window.scrollY );
    ;

    // Listen for location changes and set the scroll position accordingly.
    const unregister = history.listen((location, action) => 
      window.scrollTo(0, action !== 'POP' ? 0 : location.state?.scroll ?? 0);
    );

    // Unregister listener when component unmounts.
    return () => 
      unregister();
    ;
  , [history]);


function App(): JSX.Element 
  useScrollMemory();

  return <div>My app</div>;

使用此覆盖解决方案,您无需担心在所有 Link 元素中传递状态。一个改进是使其通用,因此它向后兼容 historypushreplace 方法,但在我的特定情况下这不是必需的,所以我省略了它。

我使用的是react-router-dom,但您也可以轻松地覆盖原版 historyAPI 的方法。

【讨论】:

【参考方案3】:

另一种可能的解决方案是将您的 /cards/:id 路由呈现为全屏模式,并将 /cards 路由安装在其后面

【讨论】:

【参考方案4】:

对于使用 Redux 的完整实施,您可以在 CodeSandbox 上查看。

我通过使用历史 API 做到了这一点。

    路线更改后保存滚动位置。

    当用户点击返回按钮时恢复滚动位置。

getSnapshotBeforeUpdate中保存滚动位置,在componentDidUpdate中恢复。

  // Saving scroll position.
  getSnapshotBeforeUpdate(prevProps) 
    const 
      history:  action ,
      location:  pathname 
     = prevProps;

    if (action !== "POP") 
      scrollData =  ...scrollData, [pathname]: window.pageYOffset ;
    

    return null;
  

  // Restore scroll position.
  componentDidUpdate() 
    const 
      history:  action ,
      location:  pathname 
     = this.props;

    if (action === "POP") 
      if (scrollData[pathname]) 
        setTimeout(() =>
          window.scrollTo(
            left: 0,
            top: scrollData[pathname],
            behavior: "smooth"
          )
        );
       else 
        setTimeout(window.scrollTo( left: 0, top: 0 ));
      
     else 
      setTimeout(window.scrollTo( left: 0, top: 0 ));
    
  

【讨论】:

这个解决方案适用于我的几个小模组。我没有在 getSnapshotBeforeUpdate 中更新 scrollData 对象,而是将 debounced 事件处理程序绑定到“scroll”事件,这样我就不会那么频繁地更新 scrollData。我还修改了 componentDidUpdate 函数,以便仅在 pathname !== prevProp.location.pathname 时滚动窗口,这样我就不会因为其他与路径无关的属性更改而滚动窗口以进行更新。 为什么在上面的代码中需要一个setTimeout?【参考方案5】:

我在我的一个 React 项目中遇到了类似的问题,我们在其中使用了功能组件。我使用@Shortchange@Agus Syahputra 提供的答案创建了一个解决方案,因为提供的确切解决方案在我的情况下由于某种原因不起作用。

我根据@Shortchange's 的答案创建了一个useScrollMemory 自定义钩子,但做了一些小的改动。这里的useScrollMemory 函数将scrollData 对象作为参数,它是一个全局对象,根据@Agus Syahputra's 答案存储访问路径名的滚动位置。 scrollData 对象在 App 组件中初始化。

useScrollMemory.js

import  useEffect  from 'react';
import  useHistory  from 'react-router-dom';

/**
 * @description Overrides the PUSH and REPLACE methods in history to
 * save the window scroll position for the route.
 *
 * @param  Object  scrollData - Contains pathname and its scroll position.
 */
const useScrollMemory = (scrollData) => 
  const history = useHistory();

  useEffect(() => 
    const  push, replace  = history;

    // Override the history PUSH method to automatically set scroll state.
    history.push = (path, state = ) => 
      scrollData[history.location.pathname] = window.scrollY;
      push(path, state);
    ;

    // Override the history REPLACE method to automatically set scroll state.
    history.replace = (path, state = ) => 
      scrollData[history.location.pathname] = window.scrollY;
      replace(path, state);
    ;

    // Listen for location changes and set the scroll position accordingly.
    const unregister = history.listen((location, action) => 
      window.scrollTo(
        0,
        action !== 'POP' ? 0 : scrollData[location.pathname] ?? 0,
      );
    );

    // Unregister listener when component unmounts.
    return () => 
      unregister();
    ;
  , [history]);
;

export default useScrollMemory;

并在App组件中调用开头的useScrollMemory:

App.js

import useScrollMemory from '../../hooks/useScrollMemory';

const scrollData = ;

const App = () => 
  useScrollMemory(scrollData);

  return <div>My app</div>;


export default App;

【讨论】:

以上是关于React Router v4 - 切换组件时保持滚动位置的主要内容,如果未能解决你的问题,请参考以下文章

react-router v4.2.2 开关不工作;始终显示主要组件

react-router:从组件或存储更改路由

使用 react-router-dom v4 在 redux 操作中重定向

如何使用 React Router V4 从 axios 拦截器重定向?

React Router 4 和酶

将道具传递给组件,React Router V4