Reputation: 63758
How can I use react-router, and have a link navigate to a particular place on a particular page? (e.g. /home-page#section-three
)
Details:
I am using react-router
in my React app.
I have a site-wide navbar that needs to link to a particular parts of a page, like /home-page#section-three
.
So even if you are on say /blog
, clicking this link will still load the home page, with section-three scrolled into view. This is exactly how a standard <a href="/home-page#section-three>
would work.
Note: The creators of react-router have not given an explicit answer. They say it is in progress, and in the mean time use other people's answers. I'll do my best to keep this question updated with progress & possible solutions until a dominant one emerges.
Research:
How to use normal anchor links with react-router
This question is from 2015 (so 10 years ago in react time). The most upvoted answer says to use HistoryLocation
instead of HashLocation
. Basically that means store the location in the window history, instead of in the hash fragment.
Bad news is... even using HistoryLocation (what most tutorials and docs say to do in 2016), anchor tags still don't work.
https://github.com/ReactTraining/react-router/issues/394
A thread on ReactTraining about how use anchor links with react-router. This is no confirmed answer. Be careful since most proposed answers are out of date (e.g. using the "hash" prop in <Link>
)
Upvotes: 116
Views: 155887
Reputation: 2585
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
export function useScrollToAnchor() {
const { pathname, hash, key } = useLocation()
useEffect(() => {
if (hash === '') window.scrollTo(0, 0)
else {
setTimeout(() => {
const id = hash.replace('#', '')
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({
block: 'start',
inline: 'nearest',
behavior: 'smooth',
})
}
}, 0)
}
}, [pathname, hash, key])
}
Need an offset because of a fixed position header?
<div id="your-anchor" style={{ scrollMarginTop: 52 }}> // Use header height here
...
</div>
Upvotes: 0
Reputation: 1683
I know it's old but in my latest [email protected], 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>
Upvotes: 1
Reputation: 21
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
Upvotes: 2
Reputation: 5706
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>
Upvotes: 69
Reputation: 19772
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):
#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);
Upvotes: 29
Reputation: 668
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')
)
Upvotes: 6
Reputation: 5673
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();
}
}
Upvotes: 9
Reputation: 1369
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
:
Upvotes: 90
Reputation: 121
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
Upvotes: 1
Reputation: 63758
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
Upvotes: 29
Reputation: 237
<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
Upvotes: 5
Reputation: 22062
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)
Upvotes: 1
Reputation: 7369
Just avoid using react-router for local scrolling:
document.getElementById('myElementSomewhere').scrollIntoView()
Upvotes: 14