简体   繁体   中英

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.

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;
    }
}

I've created a library called react-scroll-manager that addresses this issue and the other issues around scroll position with React Router. It uses this technique to navigate to hash links anywhere in the document without the need to wrap them individually. Simply wrap your Router component in a ScrollManager component:

class App extends React.Component {
  constructor() {
    super();
    this.history = createHistory();
  }
  render() {
    return (
      <ScrollManager history={this.history}>
        <Router history={this.history}>
          ...
        </Router>
      </ScrollManager>
    );
  }
}

You can link to any component with an id property:

<MyComponent id="mycomp">...</MyComponent>

Just include the id as a fragment in your Link target:

<Link to="#mycomp">...</Link>

The library is based on HTML5 and React 16, and it supports React Router 4 (and possibly earlier versions).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM