Jinjoe
Jinjoe

Reputation: 61

React Router not re rendering even though the Link URL changes

So I have a simple React App, of Posts. I have a component called Post.js. Which loads one single post.

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Spinner from '../../common/Spinner';
import { getPost } from '../../../actions/postActions';
import PostDetails from './Post-Details';
import CommentForm from './CommentForm';
import CommentFeed from './CommentFeed';
import Navbar from "../../../components/layout/Navbar";



class Post extends Component {


  componentDidMount() {
    this.props.getPost(this.props.match.params.id);
  }

  render() {

    const { post, loading } = this.props.post;
    const { user } = this.props.auth;
    let postContent;

    if (post === null || loading || Object.keys(post).length === 0) {
      postContent = <Spinner />;
    } else if (user === null || Object.keys(user).length === 0) {
      postContent = (
        <div>
        <PostDetails post={post} showActions={false}/>
        <div className="commentsArea">
          <br />
          <CommentFeed postId={post._id} comments={post.comments} />
        </div>
      </div>
      )

    } else {
        postContent = (
          <div>
            <PostDetails post={post} showActions={false}/>
            <div className="commentsArea">

              <CommentForm postId={post._id} />
              <CommentFeed postId={post._id} comments={post.comments} />
            
            </div>
          </div>
        );
    }


    return (
      <div className="postSingle">
        <Navbar />

        <div className="container">
          <div className="row">
            <div className="col-md-12">
              {postContent}
            </div>
          </div>
        </div>
      </div>
    );
    
  }
}

Post.propTypes = {
  getPost: PropTypes.func.isRequired,
  post: PropTypes.object.isRequired
};

const mapStateToProps = state => ({
  post: state.post,
  auth: state.auth
});

export default connect(mapStateToProps, { getPost })(Post);


This component has few child components, among them PostDetails.js which shows actual details of the Post, like the Picture, headline body text, author, and tags.


import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import classnames from "classnames";
import Related from "./RelatedPosts"
import { Link } from "react-router-dom";
import { deletePost, addLike, removeLike } from "../../../actions/postActions";
import Moment from "react-moment";
import Truncate from "react-truncate";

export class PostItem extends Component {
  onDeleteClick(id) {
    this.props.deletePost(id);
  }

  onLikeClick(id) {
    this.props.addLike(id);
  }

  onUnlikeClick(id) {
    this.props.removeLike(id);
  }

  findUserLike(likes) {
    const { auth } = this.props;

    if (likes.filter(like => like.user === auth.user.id).length > 0) {
      return true;
    } else {
      return false;
    }
  }

  render() {
    const { post, auth, showActions } = this.props;
    const { isAuthenticated, user } = this.props.auth;

    const tags = post.tags.map((tag, index) => (
      <div key={index} className="p-3">
        <i className="fa fa-check" /> {tag}
      </div>
    ));

    const editbtn = (
      <Link to={{
        pathname:'/editpost',
        postProps:{
          id:post._id
        }
      }}>
        Edit
      </Link>
    )


    return (
      <React.Fragment>

        <div className="row">

          
          <div className="card card-body col-md-8">
            <div className="row">
              <div className="cardHeaderImage col-md-12 ">
                {post.headerimage === "" ? (
                  "  "
                ) : (
                  <img
                    src={post.headerimage}
                    className="card-img-top"
                    alt="..."
                  />
                )}
              </div>

              <div className="postHeadline col-md-12 ">
                <h3>
                  <span className="headlineTitle">{post.headline}</span>
                  <span className="commentsCount">
                    {post.comments.length} Comments
                  </span>
                </h3>
              </div>
            </div>

            <div className="postAuthor row">
              <h6>By {post.user.name}</h6>

              {/* <img src={post.avatar} alt=""/> */}

              <h6>
                Posted <Moment format="MM/DD/YYYY">{post.date}</Moment>
              </h6>

              {isAuthenticated && user.name === post.user.name ? editbtn : ""}
            </div>

            <div className="row">
              <div className="postText col-md-12">
                <p className="card-text">{post.text}</p>

                <br />

                <h3>Tags</h3>
                <p style={{ display: "flex" }}>{tags}</p>

                {showActions ? (
                  <span className="profileActions">
                    <button
                      onClick={this.onLikeClick.bind(this, post._id)}
                      type="button"
                      className="btn btn-concord mr-1"
                    >
                      <i
                        className={classnames("fas fa-thumbs-up", {
                          "text-info": this.findUserLike(post.likes),
                        })}
                      />
                      <span className="badge badge-concord">
                        {post.likes.length}
                      </span>
                    </button>

                    <button
                      onClick={this.onUnlikeClick.bind(this, post._id)}
                      type="button"
                      className="btn btn-concord mr-1"
                    >
                      <i className="text-secondary fas fa-thumbs-down" />
                    </button>

                    <Link to={`/post/${post._id}`} className="btn btn-sole mr-1">
                      Read More
                    </Link>

                    {post.user._id === auth.user.id ||
                    post.user === auth.user.id ? (
                      <button
                        onClick={this.onDeleteClick.bind(this, post._id)}
                        type="button"
                        className="btn btn-danger mr-1"
                      >
                        <i className="fas fa-times" />
                      </button>
                    ) : null}
                  </span>
                ) : null}
              </div>
            </div>
          </div>

          <Related tags={post.tags} postId={post} />
        

        </div>




        
      </React.Fragment>
    );
  }
}

PostItem.defaultProps = {
  showActions: true
};

PostItem.propTypes = {
  deletePost: PropTypes.func.isRequired,
  addLike: PropTypes.func.isRequired,
  removeLike: PropTypes.func.isRequired,
  post: PropTypes.object.isRequired,
  auth: PropTypes.object.isRequired
};

const mapStateToProps = state => ({
  profile: state.profile,
  auth: state.auth
});

export default connect(
  mapStateToProps,
  { deletePost, addLike, removeLike }
)(PostItem);


And PostDetails has child component (RelatedPosts.js) which renders details of posts that are related to the currently loaded post. I use post tags of the current post and query for other posts on the back end that has matching tags. If any are found the preview details of those related posts will show up in RelatedPosts.js. Which is shown as a sidebar, in the PostDetail.js component

import React, { Component } from 'react'
import { connect } from 'react-redux';
import { getRelatedPosts } from '../../../actions/postActions';
import Truncate from "react-truncate";
import { Link } from "react-router-dom";



export class RelatedPosts extends Component {

  componentDidMount() {

    const { tags, postId } = this.props;
    this.props.getRelatedPosts(tags);
  }
  

  render() {

    const { tags, postId } = this.props;
    const { posts } = this.props.post;

    let related = posts.map(post => {
      return (
        <div className="card" style={{ marginBottom: "20px" }}>
          <img className="card-img-top" src={post.headerimage} alt="" />
          <div className="card-body">
            <h4 className="card-title">{post.headline}</h4>
            <p className="card-text">
              <Truncate lines={2} ellipsis={<span>...</span>}>
                {post.text}
              </Truncate>
            </p>
            <Link to={`/post/${post._id}`} className="btn btn-sole mr-1">
                  Read More
            </Link>
            
          </div>
        </div>
      );
    });

    return (
      <div className="col-md-3 offset-1 card related">
      <h3>Related Posts</h3>


    <ul>
      {related}
    </ul>

    </div>
    )
  }
}

const mapStateToProps = state => ({
  post: state.post,
  auth: state.auth
});

export default connect(mapStateToProps, { getRelatedPosts })(RelatedPosts);

Hopefully my explanation of my code structure above made sense. All of this above works just fine. However when I click on a link in one of the related posts.

From RelatedPosts.js

<Link to={`/post/${post._id}`} className="btn btn-sole mr-1">
   Read More
</Link>

The Link URL changes appropriately, but nothing is re-rendered. The same Post is still on the screen. However when I refresh the page it will then render the related post that I clicked on into view. I'm not sure why this the case.

This is the relevant code from my App.js component which contains my router

import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import jwt_decode from 'jwt-decode';
import setAuthToken from './utils/setAuthToken';
import { setCurrentUser, logoutUser } from './actions/authActions';
import { clearCurrentProfile } from './actions/profileActions';

import { Provider } from 'react-redux';
import store from './store';
import ScrollToTop from './ScrollToTop'

import PrivateRoute from './components/common/PrivateRoute';
import AuthorRoute from './components/common/AuthorRoute';
import Posts from './components/posts/all-users-posts/AllPosts';
import Post from './components/posts/post-single/Post';
import CreatePost from './components/posts/post-creation/CreatePost';
import EditPost from './components/posts/post-creation/EditPost';

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Router>
         <ScrollToTop>
          <React.Fragment>
             <Route exact path="/register" component={Register} />
              <Route exact path="/login" component={Login} />
              <Route exact path="/profiles" component={Profiles} />
              <Route exact path="/profile/:handle" component={Profile} />
              <Route exact path="/post/:id" component={Post} />
              <Route exact path="/allposts" component={Posts} />
              <Route exact path="/editpost/" component={EditPost} />

          </React.Fragment>
         </ScrollToTop>
        </Router>
      </Provider>

      );
  }
}

export default App;

This is not the entire code for my React but the code that's relevant to my question. The only thing I can think of is that when I click on the link even though the URL changes, it the still uses the same component: <Route exact path="/post/:id" component={Post} /> Maybe that is at least part of the issue.

Upvotes: 0

Views: 95

Answers (1)

gotrongluan
gotrongluan

Reputation: 40

This is long question with messy Component naming. I'm lazy to read all but I think my answer is correct. That's because:

  • route '/post/:id' is match with Post component
  • The Link is inside Post component

So when you click on it (you're in Post component), React don't unmount and mount Post component again. It just UPDATE it. So it will call update lifecycle methods such as: shouldComponentUpdate, render, componentDidUpdate... of Post

So the solution is using componentDidUpdate method for get the new postId and fetch data of Post again

Upvotes: 1

Related Questions