Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In react router v4 how does one link to a fragment identifier?

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.

like image 849
Chris Avatar asked Oct 24 '16 11:10

Chris


People also ask

How do I use links in React router?

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.

How do I route a ID in React?

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!

Which prop allows to access parameters passed in routing with React router v4?

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 .

How does React fragment work?

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.


2 Answers

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'
like image 99
Paul S Avatar answered Sep 21 '22 07:09

Paul S


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;
    }
}
like image 26
Chris Avatar answered Sep 22 '22 07:09

Chris