Kehkashan Fazal
Kehkashan Fazal

Reputation: 781

React: Stop page reload on clicking on anchor tag for scrolling

I am currently using react to create a Wikipedia style website. For data entry reasons, I am fetching an entire HTML from the database and then using dangerouslySetInnerHTML to set some part of it, like so:

dangerouslySetInnerHTML={{ __html: this.props.section.text }}

Now there are other parts of the page that have a video playing in it, however every time I click on an <a href="#id> style tag (set using the database) to navigate to another part of the same page, the entire page refreshes.

This creates a problem since the video reloads as well and starts playing from the beginning.

Is there any way to use a dangerously set anchor tag to scroll to parts of a page in React without full page reload?

Edit: Using following versions:

"react": "^16.0.0", "react-router-dom": "^4.2.2"

Upvotes: 3

Views: 14923

Answers (5)

Steven
Steven

Reputation: 3813

To add onto what @Shishir Arora was saying, instead of using history.pushState({},'',href); use something like document.getElementById(hash.replace('#', '')).scrollIntoView();.

Here's code for a full component where I leverage jQuery:

import React, { useEffect } from 'react';
import $ from 'jquery';

const handleAnchorClick = e => {
  e.preventDefault();
  e.stopPropagation();

  const hash = e.currentTarget.hash;
  document.getElementById(hash.replace('#', '')).scrollIntoView();
  // Alternatively use jQuery for the above line: $(hash).get(0).scrollIntoView();
};

export default ({ response, url, search }) => {
  useEffect(() => {
    $('#article-content').on('click', 'a[href^="#"]', handleAnchorClick);

    return () => {
      $('#article-content').off('click', 'a[href^="#"]', handleAnchorClick);
    };
  }, [response]);

  return (
    <div>
      <div id='article-content' dangerouslySetInnerHTML={{__html: response}} />
    </div>
  );
};

jQuery was helpful in capturing the currentTarget which won't get captured the same way with @Shishir's original answer because of an element nested within the tag.

Upvotes: 0

Shishir Arora
Shishir Arora

Reputation: 5923

catch default eventhandler and handle event handling yourself. you can put this in componentDidMount lifecycle hook. Also remove these handlers in componentWillUnmount

var aTags = document.querySelectorAll('a[href]'); //use apt selector for you section


     aTags.forEach(aTag =>aTag.addEventListener('click',function(e){
          e.preventDefault();
          e.nativeEvent.stopImmediatePropagation()
          e.stopPropagation()
          var href = e.target.href;
          history.pushState({},'',href);
        }));

Upvotes: 1

Chris
Chris

Reputation: 59511

What you could do is to add an eventListener to your component which listens to all anchor tags whose href attribute value start with #.

Inside your componentDidMount() method, select all your <a> tags where href="#...". You can do this with document.querySelectorAll("a[href^='#']"). That will return an array of all nodes that were matched. You can then loop through them and attach an eventListener to each one.

The eventListener would listen to click and run a function which prevents the default behavior (redirect to another page) and instead push to your router. (Remove the comment below)

class MyApp extends React.Component {

  constructor(props) {
    super(props);
  }

  componentDidMount() {
    document.querySelectorAll("a[href^='#']").forEach(node => {
      node.addEventListener('click', e => {
        e.preventDefault();
        console.log(e.target.href);
        //this.props.history.push(e.target.href);
      });
    })
  }
 
  render() {
    const myMarkup = "<ul><li><a href='#'>Page link 1</a></li><li><a href='#test'>Page link 2</a></li><li><a href='https://google.com'>External link</a></li></ul>";
    return(
      <div id="container" dangerouslySetInnerHTML={{__html: myMarkup}}></div>
    );
  }
}
 
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

The above snippet assumes you are using the withRouter HOC. Otherwise the push won't work. For more info, see my answer to How to push to History in React Router v4?.

Upvotes: 3

asubanovsky
asubanovsky

Reputation: 1748

Since you're using React Router v4, why don't you use Link?

...
import { Link } from 'react-router-dom';
... 
<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: { fromDashboard: true }
}}/>

You can find the documentation here.

Upvotes: -2

Sotiris Kiritsis
Sotiris Kiritsis

Reputation: 3336

Since your question has the tag "react-router" I assume you use it in your application, I also assume you are using the latest version which v4.x.

The way I have tackled this in the past was using React Router Hash link package. As is noted in their documentation, you can use it like that (replace your <a href="#id"> with <Link>):

// In YourComponent.js
...
import { HashLink as Link } from 'react-router-hash-link';
...
// Use it just like a RRv4 link (to can be a string or an object, see RRv4 api for details):
<Link to="/some/path#with-hash-fragment">Link to Hash Fragment</Link>

Upvotes: -1

Related Questions