Given the very simple page (having assumed React and react-router@4 have been imported):
// Current location: example.com/about
<Link to="/about/#the-team">See the team</Link>
// ... loads of content ... //
<a id="the-team"></a>
I would expect the above, upon clicking "See the team" would scroll down to the id'ed team anchor. The url correctly updates to: example.com/about#the-team
, but it doesn't scroll down.
I have tried alternatives such as <a name="the-team"></a>
but I believe this is no longer spec (nor does it work).
There are plenty of work arounds on github for react-router@v2 but they rely on the update callback present on BrowserRouter that is no longer present in v4.
To add the link in the menu, use the <NavLink /> component by react-router-dom . The NavLink component provides a declarative way to navigate around the application. It is similar to the Link component, except it can apply an active style to the link if it is active.
Use the useParams() hook to get the ID from a URL in React, e.g. const params = useParams() . The useParams hook returns an object of key-value pairs of the dynamic params from the current URL that were matched by the Route path. Copied!
Whenever React Router v4 renders a component, it'll pass to that component three props, match , location , and history . For our use case, we can grab the URL parameter ( handle ) as a property on match. params .
React Fragments allow you to wrap or group multiple elements without adding an extra node to the DOM. This can be useful when rendering multiple child elements/components in a single parent component.
Given a <ScrollIntoView>
component which takes the id of the element to scroll to:
class ScrollIntoView extends React.Component {
componentDidMount() {
this.scroll()
}
componentDidUpdate() {
this.scroll()
}
scroll() {
const { id } = this.props
if (!id) {
return
}
const element = document.querySelector(id)
if (element) {
element.scrollIntoView()
}
}
render() {
return this.props.children
}
}
You could either wrap the contents of your view component in it:
const About = (props) => (
<ScrollIntoView id={props.location.hash}>
// ...
</ScrollIntoView>
)
Or you could create a match wrapper:
const MatchWithHash = ({ component:Component, ...props }) => (
<Match {...props} render={(props) => (
<ScrollIntoView id={props.location.hash}>
<Component {...props} />
</ScrollIntoView>
)} />
)
The usage would be:
<MatchWithHash pattern='/about' component={About} />
A fully fleshed out solution might need to consider edge cases, but I did a quick test with the above and it seemed to work.
This component is now available through npm. GitHub: https://github.com/pshrmn/rrc
npm install --save rrc
import { ScrollIntoView } from 'rrc'
The react-router team seem to be actively tracking this issue (at the time of writing v4 isn't even fully released).
As a temporary solution, the following works fine.
EDIT 3 This answer can now be safely ignored with the accepted answer in place. Left as it tackles the question slightly differently.
EDIT2 The following method causes other issues, including but not limited to, clicking Section A, then clicking Section A again doesn't work. Also doesn't appear to work with any kind of animation (have a feeling with animation starts, but is overwritten by a later state change)
EDIT Note the following does screw up the Miss component. Still looking for a more robust solution
// App
<Router>
<div>
<Match pattern="*" component={HashWatcher} />
<ul>
<li><Link to="/#section-a">Section A</Link></li>
<li><Link to="/#section-b">Section B</Link></li>
</ul>
<Match pattern="/" component={Home} />
</div>
</Router>
// Home
// Stock standard mark up
<div id="section-a">
Section A content
</div>
<div id="section-b">
Section B content
</div>
Then, the HashWatcher component would look like the following. It is the temp component that "listens" for all route changes
import { Component } from 'react';
export default class HashWatcher extends Component {
componentDidMount() {
if(this.props.location.hash !== "") {
this.scrollToId(this.hashToId(this.props.location.hash));
}
}
componentDidUpdate(prevProps) {
// Reset the position to the top on each location change. This can be followed up by the
// following hash check.
// Note, react-router correctly sets the hash and path, even if using HashHistory
if(prevProps.location.pathname !== this.props.location.pathname) {
this.scrollToTop();
}
// Initially checked if hash changed, but wasn't enough, if the user clicked the same hash
// twice - for example, clicking contact us, scroll to top, then contact us again
if(this.props.location.hash !== "") {
this.scrollToId(this.hashToId(this.props.location.hash));
}
}
/**
* Remove the leading # on the hash value
* @param string hash
* @return string
*/
hashToId(hash) {
return hash.substring(1);
}
/**
* Scroll back to the top of the given window
* @return undefined
*/
scrollToTop() {
window.scrollTo(0, 0);
}
/**
* Scroll to a given id on the page
* @param string id The id to scroll to
* @return undefined
*/
scrollToId(id) {
document.getElementById(id).scrollIntoView();
}
/**
* Intentionally return null, as we never want this component actually visible.
* @return {[type]} [description]
*/
render() {
return null;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With