7

Lets say I have component "Post" which holds multiple components "Comment". I want to make that application scrolls down on comment with that anchor when I enter URL like this:

/post/:postId/#commentId

I have already working postId route /post/:postId

I tried to implement it with react-hash-link npm package but it's not working as intended.

Every comment has it's own ID which is set on component, like this:

<div class="post">
   <div class="post-header">
      <div class="post-header-avatar">
        SOME TEXT
      </div>
      <div class="post-header-info">
        SOME TEXT
      </div>
   </div>
   <div class="post-content">
      <span>POST CONTENT</span>
   </div>
   <div class="post-likers-container">
      <div class="post-likers-header label">People who like this post</div>
      <div class="post-likers">
          SOME TEXT
      </div>
   </div>
   <div class="post-comments">
      <div class="comments ">
         <div class="comments-all label">Comments</div>
         <div class="comments">
            <div class="comment" id="5d27759edd51be1858f6b6f2">
               <div class="comment-content">
               COMMENT 1 TEXT
               </div>
            </div>
            <div class="comment" id="5d2775b2dd51be1858f6b720">
               <div class="comment-content">
               COMMENT 2 TEXT
               </div>
            </div>
            <div class="comment" id="5d2775ecdd51be1858f6b753">
               <div class="comment-content">
                COMMENT 3 TEXT
               </div>
            </div>
         </div>
      </div>
   </div>
</div>

So for example if I open URL like:

/post/postId/#5d2775ecdd51be1858f6b753 

I want to open page of post and that it scrolls down to the comment with # anchor.

Is there any way to implement this?

Ed Bangga
  • 12,879
  • 4
  • 16
  • 30
SaltyTeemooo
  • 113
  • 1
  • 7

5 Answers5

3

I managed to find simple solution for my use case, without creating refs for comments, passing them, etc. Since my hierarchy of components is like this:

  1. Post --> render component Comments
  2. Comments --> render multiple components Comment with props data passed from Post

In Post component I created function:

scrollToComment= () => {
    let currentLocation = window.location.href;
    const hasCommentAnchor = currentLocation.includes("/#");
    if (hasCommentAnchor) {
      const anchorCommentId = `${currentLocation.substring(currentLocation.indexOf("#") + 1)}`;
      const anchorComment = document.getElementById(anchorCommentId);
      if(anchorComment){
          anchorComment.scrollIntoView({ behavior: "smooth" });
      }
    }
  }

Then I render Comments like this:

<Comments limit={limit} post={post} scrollToComment={this.scrollToComment} />

In Comments I generate comments after some sorting like this:

{sortedComments.map((comment, i) => <Comment key={i} {...comment} scrollToComment={this.props.scrollToComment}/> )}

and finally in Comment component I execute scrollToComment in ComponentDidMount():

if(this.props.scrollToComment)
    this.props.scrollToComment(this.props._id);

After that when I go to some URL I get nice smooth scrolling to the comment specified in hash part of URL.

I tried @Christopher solution but it didn't worked for me.

SaltyTeemooo
  • 113
  • 1
  • 7
3

I really liked your solution @SaltyTeemooo. Inspired by it I found an even simpler way without any callbacks.

My setup is very similar so lets say I am dealing with posts and comments.

In Post I create the Comments (simpified) like this and pass the anchorId:

<Comments anchorId={window.location.href.slice(window.location.href.indexOf("#") + 1)} props... />

In Comments I pass the anchor id along into Comment.js

<Comment anchorId={props.anchorId} props.../>

And then in the Comment, I scroll the current element into view, if it is the linked one

import React, { useRef, useEffect } from 'react';

function Comment () {

    const comment = useRef(null); //to be able to access the current one

    useEffect(() => {
        if(props.anchorId === props.commentData.id)
        {
            comment.current.scrollIntoView({ behavior: "smooth" });
        }
    }, []) //same as ComponentDidMount

    
    return(
       <div id={props.commentData.id} ref={comment}> //here is where the ref gets set
           ...
       </div>
    )
}
tina
  • 51
  • 4
1

Took a pretty solid amount of time but try this sandbox: https://codesandbox.io/s/scrollintoview-with-refs-and-redux-b881s

This will give you a ton of insight on how to scroll to an element using a URL param.

import React from "react";
import { connect } from "react-redux";
import { getPost } from "./postActions";

class Post extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeComment: null
    };

    this._nodes = new Map();
  }

  componentDidMount() {
    this.props.getPost(this.props.match.params.id);
    const path = window.location.href;
    const commentId = path.slice(path.indexOf("#") + 1);
    if (commentId) {
      this.setState({
        activeComment: commentId
      });
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.activeComment !== prevState.activeComment) {
      this.scrollToComment();
    }
  }

  scrollToComment = () => {
    const { activeComment } = this.state;
    const { comments } = this.props.posts.post;
    const nodes = [];
    //Array.from creates a new shallow-copy of an array from an array-like or iterable object
    Array.from(this._nodes.values()) //this._nodes.values() returns an iterable-object populated with the Map object values
      .filter(node => node != null)
      .forEach(node => {
        nodes.push(node);
      });

    const commentIndex = comments.findIndex(
      comment => comment.id == activeComment
    );

    if (nodes[commentIndex]) {
      window.scrollTo({
        behavior: "smooth",
        top: nodes[commentIndex].offsetTop
      });
    }
  };

  createCommentList = () => {
    const { post } = this.props.posts;
    const { activeComment } = this.state;

    if (post) {
      return post.comments.map((comment, index) => {
        return (
          <div
            key={comment.id}
            className={
              "comment " + (activeComment == comment.id ? "activeComment" : "")
            }
            ref={c => this._nodes.set(comment.id, c)}
          >
            {comment.text}
          </div>
        );
      });
    }
  };

  displayPost = () => {
    const { post } = this.props.posts;

    if (post) {
      return (
        <div className="post">
          <h4>{post.title}</h4>
          <p>{post.text}</p>
        </div>
      );
    }
  };

  render() {
    return (
      <div>
        <div>{this.displayPost()}</div>
        <div>{this.createCommentList()}</div>
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    posts: state.posts
  };
};

const mapDispatchToProps = dispatch => {
  return {
    getPost: postId => {
      dispatch(getPost(postId));
    }
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Post);
Chris Ngo
  • 15,460
  • 3
  • 23
  • 46
1

In my simple case where there is no async content loading, I got the desired scrolling behavior by just adding this at the top of the page:

useEffect(() => {
    const href = window.location.href
    if (href.includes("#")) {
      const id = `${href.substring(href.indexOf("#") + 1)}`
      const anchor = document.getElementById(id)
      if(anchor){
          anchor.scrollIntoView({ behavior: "smooth" })
      }
    }
}, [])

FYI, this was for some FAQ pages consisting of a bunch of FaqEntry objects, each with a question and answer. The code below allows linking to individual entries that initialize with the answer open.

export default function FaqEntry({title, history, children}) {
if(!history) console.log("OOPS, you forgot to pass history prop", title)

const  createName = title => title.toLowerCase().replace(/[^\sa-z]/g, "").replace(/\s\s*/g, "_")
const id = createName(title)

const href = window.location.href
const isCurrent = href.includes("#") && href.substring(href.indexOf("#") + 1) === id
const [open, setOpen] = useState(isCurrent)

function handleClick() {
    setOpen(!open)
    if (history && !open) {
        const pathname = window.location.pathname + "#" + id
        history.replace(pathname)
    }
}
return <div id={id} className={`faqEntry ${open ? "open" : "closed"}`}>
    <div className="question" onClick={handleClick}>{title}</div>
    <div className="answer">{children}</div>
</div>

}

I pass the history object from React Router so that I can update the browser history without triggering a page reload.

Jay Cincotta
  • 4,298
  • 3
  • 21
  • 17
0

Mensure...

import React, { useEffect } from 'react';

const MainApp = () => {

    const MyRef = React.createRef();

    useEffect(() => { // Same like ComponentDidMount().
        scrollTo();
    })

    const scrollTo = () => {
        window.scrollTo({
            top:myRef.offsetTop, 
            behavior: "smooth" // smooth scroll.
        });   
    }

        return (
            <div ref={MyRef}>My DIV to scroll to.</div>
        )
}
Galanthus
  • 1,958
  • 3
  • 14
  • 35