我有两个使用react-router创建的 <Route>
。
- /cards -> 卡牌游戏列表
- /cards/1 -> 卡牌游戏#1详情
当用户点击"返回列表"时,我想滚动到他在列表中的位置。
我该怎么做?
我有两个使用react-router创建的 <Route>
。
当用户点击"返回列表"时,我想滚动到他在列表中的位置。
我该怎么做?
在codesandbox中有一个工作示例。
React Router v4没有为滚动恢复提供开箱即用的支持,而且目前他们也不会提供。您可以在他们文档的React Router V4 - 滚动恢复部分了解更多信息。
所以,每个开发人员都需要编写逻辑来支持这一点,尽管我们有一些工具来使之生效。
.scrollIntoView()
可在元素上调用,如您所料,它将其滚动到视图中。支持相当好,目前,97%的浏览器都支持它。来源:icanuse
React Router的Link组件有一个to
属性,您可以提供一个对象而不是一个字符串。下面是这个属性的样式。
<Link to={{ pathname: '/card', state: 9 }}>Card nine</Link>
我们可以使用状态将信息传递给将要呈现的组件。在这个例子中,状态被赋予一个数字,它将足以回答你的问题,稍后你会看到,但它可以是任何内容。路由/card
呈现<Card />
现在可以访问变量状态在props.location.state,我们可以随意使用它。
在呈现各种卡片时,我们为每个卡片添加一个唯一的类。这样我们就有了一个标识符,我们可以传递并知道这个项目需要在导航回卡片列表概述时滚动到视图中。
<Cards />
呈现一个列表,每个条目都有一个唯一的类;Link />
将唯一标识符传递给<Card />
;<Card />
呈现卡片详细信息和一个带有唯一标识符的返回按钮;<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} <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);
由于在函数组件中没有解决此问题的答案,下面是我为一个项目实现的钩子解决方案:
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>;
}
使用此覆盖解决方案,您无需担心在所有元素中传递状态。改进的方法是使其通用,以便与 history 的
push 和
replace 方法向后兼容,但在我的特定情况下这不是必需的,因此我省略了它。
我正在使用react-router-dom
,但您也可以轻松地覆盖原始historyAPI的方法。
这个问题的另一个可能解决方法是将您的/cards/:id
路由呈现为全屏模式,同时保持/cards
路由在其后面挂载。
// 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 }));
}
}
在我的一个使用函数组件的React
项目中,我遇到了类似的问题。由于某些原因,提供的确切解决方案在我的情况下无法正常工作,因此我根据@Shortchange
和@Agus Syahputra
提供的答案创建了一个解决方案。
我按照@Shortchange
的答案创建了一个名为useScrollMemory
的自定义钩子,但进行了一些小修改。这里的useScrollMemory
函数接受scrollData
对象作为参数,该全局对象根据@Agus Syahputra
的答案存储已访问路径名的滚动位置。 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;
我刚遇到了这个问题。我找到了一个解决方案,似乎可以很好地解决它:
export const useKeepScrollPositionOnRouteChange = () => {
const route = useLocation();
useLayoutEffect(() => {
const { scrollY, requestAnimationFrame, scrollTo } = window;
requestAnimationFrame(() => {
scrollTo(0, scrollY);
});
}, [route]);
};
在stackoverflow上的第一篇帖子!
ScrollRestoration
组件来实现此功能。
该组件将模拟浏览器的滚动恢复功能,以确保在加载程序完成后对位置进行更改时可以将滚动位置恢复到正确的位置,即使跨域。
要保存每个不同路径的滚动位置,请将getKey
函数传递给ScrollRestoration
,该函数返回location.pathname
作为用于区分路由滚动位置的键。
在根组件中渲染ScrollRestoration
一次,以将其应用于所有路径:
import { ScrollRestoration } from 'react-router-dom';
function App() {
return <>
<div>Content</div>
<ScrollRestoration getKey={(location, matches) => location.pathname}/>
</>;
}
getKey
,则默认行为是在导航到新页面(例如通过单击链接)时将滚动位置重置为顶部,并在返回到先前页面时(例如通过单击浏览器的后退按钮)保持原始滚动位置。