In react router v4 how does one link to a fragment identifier?
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.
Edit:
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;
}
}