111

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>)


Zach
  • 539
  • 1
  • 4
  • 22
Don P
  • 60,113
  • 114
  • 300
  • 432

12 Answers12

89

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:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
maledr53
  • 1,359
  • 11
  • 7
  • This is my preferred solution – ESR Jan 29 '18 at 13:09
  • 1
    Works fine for me . Url will looks like http://localhost:8080/#!/#services – omakoleg Jun 03 '18 at 13:13
  • 1
    it also has types for the ones using typescript: npm install @types/react-router-hash-link – Nick G. Oct 17 '18 at 19:59
  • 2
    This solution does not work to progamatically push history. – FabianoLothor Sep 03 '19 at 20:52
  • 1
    This finally worked for me. It was not working because I was inserting the ID in a component, instead of a DOM element. – Luis Febro Sep 16 '19 at 20:40
  • 1
    For Yarn, of course, it's `yarn add react-router-hash-link` and then `yarn install` (or just `yarn`). – ADTC Feb 14 '23 at 16:07
  • Using `HashRouter` wasn't necessary for me on React 18 and `react-router-dom@^6.9.0`. Just using `HashLink` worked as a drop-in replacement for the regular `Link` (used in just one view so far) so that the anchors worked correctly (1. create a link to `my/subview#someAnchor`, 2. In that view, have an `
    ...`, 3. Click on the link in another view/component and it will browse to the link and scroll to that `id` as expected.
    – Juha Untinen Mar 29 '23 at 17:14
60

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>
Firmin Martin
  • 280
  • 3
  • 15
ritz
  • 4,747
  • 2
  • 15
  • 13
  • This works great. I wish this solution was more prominent! – JimmyTheCode Apr 29 '21 at 12:32
  • 1
    tnx @JimmyTheCode – ritz Jul 15 '21 at 04:05
  • 1
    Nice answer. `react-router-hash-link` didn't work greatly for me. I made an edit to improve the answer: (1) `hash` is missing as a dependency of `useEffect` (2) if we depend on `location.key`, we can guarantee that it will still scroll to the target on `` click. Use case: imagine the user clicks the `` then scroll away, clicking again the same `` won't have any effect if we do not depend on `key`. – Firmin Martin Nov 05 '21 at 17:53
  • 1
    Ah, and the 0ms timeout works fine for local route change, but from another page, it doesn't give enough time to render the target element. – Firmin Martin Nov 05 '21 at 19:07
  • 1
    I can confirm this works with `react-router` v6. `react-router-hash-link` did not work for me. – James Dec 15 '21 at 16:47
  • Thanks for this answer, I'm using react v18 and react-router-dom v6.8 and works as a treat! – Carlos Gregorio Feb 05 '23 at 11:22
29

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Don P
  • 60,113
  • 114
  • 300
  • 432
27

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):

Edit Simple React Anchor


#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);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Matt Carlotta
  • 18,972
  • 4
  • 39
  • 51
  • sweet, thx. Why `element.offsetTop` instead of `window.scrollY + element.getBoundingClientRect().top` ? The latter makes it independent from the closest relative parent. – Yannick Schuchmann Oct 10 '19 at 15:16
  • In this simple example, calculating `element.offsetTop` will essentially give you the same result as `window.scrollY` + `element.getBoundingClientRect().top`. However, if you're nesting your element within a `table`, then yes, you'll want to use the later over the former. For example, nested with `table`: https://jsfiddle.net/pLuvbyx5/, and unnested element: https://jsfiddle.net/8bwj6yz3/ – Matt Carlotta Oct 10 '19 at 17:34
  • Is there any way to avoid the **setTimeOut** ? Can we implement the same without using setTimeOut ? https://stackoverflow.com/questions/64224547/auto-scroll-to-anchor-tag-element-in-react-using-componentdidupdate – Amit Kumar Oct 06 '20 at 23:07
  • 4
    Unfortunately, no. Some browsers (like Safari) won't update the scroll position without the delay. – Matt Carlotta Oct 06 '20 at 23:11
  • @MattCarlotta let suppose my page takes more than 100ms to render will it work in that case? if yes then, please inform us a bit about it. can you please address this https://stackoverflow.com/questions/64224547/auto-scroll-to-anchor-tag-element-in-react-using-componentdidupdate – Amit Kumar Oct 07 '20 at 08:57
13

Just avoid using react-router for local scrolling:

document.getElementById('myElementSomewhere').scrollIntoView() 
alex_1948511
  • 6,073
  • 2
  • 20
  • 21
  • 2
    Ideally the local scrolling goes through the router because then you can externally link to that specific part of the document, but this answer's still great thanks, because it has told me what code I need to put in my this.props.history.listen callback. – Antony Apr 01 '18 at 09:53
  • 1
    In my case I just wanted to scroll down to a div by imitating the same as a link with href as #myElementId... this indeed was the best and simple answer, thank you! – Frederiko Ribeiro Nov 26 '20 at 10:14
9

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();
  }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
randomor
  • 5,329
  • 4
  • 46
  • 68
6

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')
)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Matthias Bohlen
  • 598
  • 6
  • 12
5
<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
Noa
  • 207
  • 4
  • 4
2

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

1

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ram
  • 111
  • 1
  • 6
1

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)
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
  • This code actually worked for me for initial page loading in a React application when I called it after my API call to get the page content. I like the simplicity of it and same page links already worked for me. – Mideus Feb 12 '21 at 22:51
1

I know it's old but in my latest react-router-dom@6.4.4, 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>
Tanvir
  • 1,642
  • 8
  • 32
  • 53
  • I just want to mention that this really helped me out. I also used it in combination with https://stackoverflow.com/a/51588820 to achieve smooth scrolling as well. Read about reloadDocument in the React Router V6 docs: https://reactrouter.com/en/main/components/link#link – AndrewA Jan 22 '23 at 03:18
  • I couldn't test this as my version is 5, and we're not upgrading yet. So I'm curious: doesn't this reload the entire page (and thus the React app) if the path is different from the current path? If it does, then wouldn't that mean we're losing the benefits of the router which changes the content shown in the React app without reloading the entire app? – ADTC Feb 14 '23 at 16:06