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。
当用户点击“返回列表”时,我想将用户滚动到他在列表中的位置。
我该怎么做?
【问题讨论】:
【参考方案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
渲染 <Card />
的路由现在可以访问 props.location.state 的变量状态,我们可以随意使用它。
标记每个列表项
在渲染各种卡片时,我们为每张卡片添加一个唯一的类。这样我们就有了一个可以传递的标识符,并且当我们导航回卡片列表概览时,我们知道需要将该项目滚动到视图中。
解决方案
<Cards />
呈现一个列表,每个项目都有一个唯一的类;
当一个项目被点击时,Link />
将唯一标识符传递给<Card />
;
<Card />
呈现卡片详细信息和带有唯一标识符的后退按钮;
单击按钮并安装<Cards />
时,.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 <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
元素中传递状态。一个改进是使其通用,因此它向后兼容 history
的 push
和 replace
方法,但在我的特定情况下这不是必需的,所以我省略了它。
我使用的是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-dom v4 在 redux 操作中重定向