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

时间:2018-08-18 08:22:12

标签: javascript reactjs scrollview react-router-v4 react-router-dom

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

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

当用户单击“返回列表”时,我想滚动显示用户在列表中的位置。

我该怎么做?

5 个答案:

答案 0 :(得分:10)

codesandbox上的工作示例

React Router v4不提供开箱即用的滚动恢复支持,就目前而言,它们也不会。在他们的文档的React Router V4 - Scroll Restoration部分中,您可以了解更多有关此内容的信息。

因此,尽管我们确实有一些工具可以使这项工作奏效,但每个开发人员都需要编写逻辑来支持这项工作。

element.scrollIntoView()

可以在元素上调用

.scrollIntoView(),您可以猜到它会滚动到视图中。支持非常好,目前有93.33%的浏览器支持它。 Source: icanuse

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

反应路由器的链接组件具有一个to道具,您可以提供一个对象而不是一个字符串。这就是这个样子。

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

我们可以使用状态将信息传递给将要呈现的组件。在此示例中,为state分配了一个数字,该数字足以回答您的问题,稍后您将看到,但可以是任何数字。渲染/card的路线<Card />现在可以在 props.location.state 处访问变量状态,我们可以根据需要使用它。

标记每个列表项

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

解决方案

  1. <Cards />呈现一个列表,每个项目都有一个唯一的类;
  2. 单击某项时,Link />将唯一标识符传递给<Card />;
  3. <Card />呈现卡的详细信息和带有唯一标识符的后退按钮;
  4. 单击按钮并安装<Cards />时,.scrollIntoView()滚动到以前使用props.location.state中的数据单击的项目。

下面是各个部分的一些代码段。

// 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);

答案 1 :(得分:3)

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

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 元素中传递状态。一个改进是使其通用,因此它向后兼容 pushreplacehistory 方法,但在我的特定情况下这不是必需的,所以我省略了它。

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

答案 2 :(得分:1)

对此的另一种可能的解决方案是将Porumbei.objects.filter(sex__sexul="male", pairs_as_male__isnull=True) 路线呈现为全屏模式,并保持/cards/:id路线安装在其后面

答案 3 :(得分:0)

对于使用Redux的全面实施,您可以在CodeSandbox上看到它。

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

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

  2. 当用户单击“后退”按钮时恢复滚动位置。

将滚动位置保存在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 }));
    }
  }

答案 4 :(得分:0)

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

我根据 useScrollMemory 答案创建了一个 @Shortchange's 自定义挂钩,但做了一些小改动。此处的 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;