使用锚与反应路由器

如何使用反应路由器,并让链接导航到特定页面上的特定位置(例如 /home-page#section-three)

详情 :

我使用 react-router在我的反应应用程序。

我有一个站点范围的导航栏,需要链接到一个页面的特定部分,如 /home-page#section-three

因此,即使你在说 /blog,点击这个链接仍然会加载主页,第三部分滚动进入视图。这正是标准 <a href="/home-page#section-three>的工作方式。

注意 : response-router 的创建者还没有给出明确的答案。他们说这是在进行中,同时使用其他人的答案。我会尽我最大的努力使这个问题与进展和可能的解决方案保持更新,直到一个占主导地位的问题出现。

研究 :


如何使用正常的锚链路与反应路由器

这个问题来自2015年(也就是10年前的反应时间)。投票最多的答案是使用 HistoryLocation而不是 HashLocation。基本上,这意味着将位置存储在窗口历史记录中,而不是散列片段中。

坏消息是... ... 即使使用 HistoryLocation (大多数教程和文档在2016年都会这么做) ,锚标记仍然不起作用。


Https://github.com/reacttraining/react-router/issues/394

关于如何使用锚链接和反应路由器的反应训练课程。这不是确定的答案。要小心,因为大多数建议的答案都过时了(例如,使用 <Link>中的“ hash”支持)


120806 次浏览

Here is one solution I have found (October 2016). It is is cross-browser compatible (tested in Internet Explorer, Firefox, Chrome, mobile Safari, and Safari).

You can provide an onUpdate property to your Router. This is called any time a route updates. This solution uses the onUpdate property to check if there is a DOM element that matches the hash, and then scrolls to it after the route transition is complete.

You must be using browserHistory and not hashHistory.

The answer is by "Rafrax" in Hash links #394.

Add this code to the place where you define <Router>:

import React from 'react';
import { render } from 'react-dom';
import { Router, Route, browserHistory } from 'react-router';


const routes = (
// your routes
);


function hashLinkScroll() {
const { hash } = window.location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) element.scrollIntoView();
}, 0);
}
}


render(
<Router
history={browserHistory}
routes={routes}
onUpdate={hashLinkScroll}
/>,
document.getElementById('root')
)

If you are feeling lazy and don't want to copy that code, you can use Anchorate which just defines that function for you. https://github.com/adjohnson916/anchorate

Just avoid using react-router for local scrolling:

document.getElementById('myElementSomewhere').scrollIntoView()

An alternative: react-scrollchor https://www.npmjs.com/package/react-scrollchor

react-scrollchor: A React component for scroll to #hash links with smooth animations. Scrollchor is a mix of Scroll and Anchor

Note: It doesn't use react-router

React Router Hash Link worked for me and is easy to install and implement:

$ npm install --save react-router-hash-link

In your component.js import it as Link:

import { HashLink as Link } from 'react-router-hash-link';

And instead of using an anchor <a>, use <Link> :

<Link to="home-page#section-three">Section three</Link>

Note: I used HashRouter instead of Router:

The problem with Don P's answer is sometimes the element with the id is still been rendered or loaded if that section depends on some async action. The following function will try to find the element by id and navigate to it and retry every 100 ms until it reaches a maximum of 50 retries:

scrollToLocation = () => {
const { hash } = window.location;
if (hash !== '') {
let retries = 0;
const id = hash.replace('#', '');
const scroll = () => {
retries += 0;
if (retries > 50) return;
const element = document.getElementById(id);
if (element) {
setTimeout(() => element.scrollIntoView(), 0);
} else {
setTimeout(scroll, 100);
}
};
scroll();
}
}

I adapted Don P's solution (see above) to react-router 4 (Jan 2019) because there is no onUpdate prop on <Router> any more.

import React from 'react';
import * as ReactDOM from 'react-dom';
import { Router, Route } from 'react-router';
import { createBrowserHistory } from 'history';


const browserHistory = createBrowserHistory();


browserHistory.listen(location => {
const { hash } = location;
if (hash !== '') {
// Push onto callback queue so it runs after the DOM is updated,
// this is required when navigating from a different page so that
// the element is rendered on the page before trying to getElementById.
setTimeout(
() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
},
0
);
}
});


ReactDOM.render(
<Router history={browserHistory}>
// insert your routes here...
/>,
document.getElementById('root')
)

Here's a simple solution that doesn't require any subscriptions nor third-party packages. It should work with react-router@3 and above and react-router-dom.

Working example: https://fglet.codesandbox.io/

Source (unfortunately, it doesn't currently work within the editor):

Edit Simple React Anchor


#ScrollHandler Hook Example

import { useEffect } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";


const ScrollHandler = ({ location, children }) => {
useEffect(
() => {
const element = document.getElementById(location.hash.replace("#", ""));


setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
}, [location]);
);


return children;
};


ScrollHandler.propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.shape({
hash: PropTypes.string,
}).isRequired
};


export default withRouter(ScrollHandler);

#ScrollHandler Class Example

import { PureComponent } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router-dom";


class ScrollHandler extends PureComponent {
componentDidMount = () => this.handleScroll();


componentDidUpdate = prevProps => {
const { location: { pathname, hash } } = this.props;
if (
pathname !== prevProps.location.pathname ||
hash !== prevProps.location.hash
) {
this.handleScroll();
}
};


handleScroll = () => {
const { location: { hash } } = this.props;
const element = document.getElementById(hash.replace("#", ""));


setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
};


render = () => this.props.children;
};


ScrollHandler.propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.shape({
hash: PropTypes.string,
pathname: PropTypes.string,
})
};


export default withRouter(ScrollHandler);

For simple in-page navigation you could add something like this, though it doesn't handle initializing the page -

// handle back/fwd buttons
function hashHandler() {
const id = window.location.hash.slice(1) // remove leading '#'
const el = document.getElementById(id)
if (el) {
el.scrollIntoView()
}
}
window.addEventListener('hashchange', hashHandler, false)
<Link to='/homepage#faq-1'>Question 1</Link>
useEffect(() => {
const hash = props.history.location.hash
if (hash && document.getElementById(hash.substr(1))) {
// Check if there is a hash and if an element with that id exists
document.getElementById(hash.substr(1)).scrollIntoView({behavior: "smooth"})
}
}, [props.history.location.hash]) // Fires when component mounts and every time hash changes

This solution works with react-router v5

import React, { useEffect } from 'react'
import { Route, Switch, useLocation } from 'react-router-dom'


export default function App() {
const { pathname, hash, key } = useLocation();


useEffect(() => {
// if not a hash link, scroll to top
if (hash === '') {
window.scrollTo(0, 0);
}
// else scroll to id
else {
setTimeout(() => {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
}, 0);
}
}, [pathname, hash, key]); // do this on route change


return (
<Switch>
<Route exact path="/" component={Home} />
.
.
</Switch>
)
}

In the component

<Link to="/#home"> Home </Link>

Create A scrollHandle component

    import { useEffect } from "react";
import { useLocation } from "react-router-dom";


export const ScrollHandler = ({ children}) => {


const { pathname, hash } = useLocation()


const handleScroll = () => {


const element = document.getElementById(hash.replace("#", ""));


setTimeout(() => {
window.scrollTo({
behavior: element ? "smooth" : "auto",
top: element ? element.offsetTop : 0
});
}, 100);
};


useEffect(() => {
handleScroll()
}, [pathname, hash])


return children
}

Import ScrollHandler component directly into your app.js file or you can create a higher order component withScrollHandler and export your app as withScrollHandler(App)

And in links <Link to='/page#section'>Section</Link> or <Link to='#section'>Section</Link>

And add id="section" in your section component

I know it's old but in my latest react-router-dom@6.4.4, this simple attribute reloadDocument is working:

div>
<Link to="#result" reloadDocument>GO TO ⬇  (Navigate to Same Page) </Link>
</div>
<div id='result'>CLICK 'GO TO' ABOVE TO REACH HERE</div>