user8907070
user8907070

Reputation:

Change in redux state does not cause change in component / componentDidUpdate not called

I have a post details component where on clicking the like button the redux state changes the redux state is like

posts ->postDetails

I'am changing the liked property and number of likes of postDetais object, On clicking the like button the liked property is set to true from false and vice versa and the number of likes is incremented.

However the state is changing but the componentDidUpdate method is not firing

PostDetails.js

import React, { Component } from "react";
import { connect } from "react-redux";
import {
  getPostData,
  likePost,
  unlikePost
} from "../../store/actions/postsActions";
import { Icon, Tooltip } from "antd";
import { Link } from "react-router-dom";

export class PostDetails extends Component {
  state = {
    postData: this.props.postDetails
  };

  componentDidMount() {
    this.props.getPostData(this.props.match.params.post_id);
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log(this.props.postDetails);
    if (prevProps.postDetails !== this.props.postDetails) {
      this.setState({
        postData: this.props.postDetails
      });
    }
  }

  render() {
    const { postData } = this.state;
    const liked = postData.liked;
    return (
      <div className="postDetails">
        {postData && (
          <div className="postDetailsContainer">
            <div className="postImage">
              <img src={postData.imageUrl} alt={postData.caption} />
            </div>
            <div className="postContent">
              <div className="postContent__header">
                <Link
                  to={`/user/${postData.username}`}
                  className="postContent__headerContent"
                >
                  <img
                    src={postData.profileUrl}
                    alt={postData.username}
                    className="postContent__profileImage"
                  />
                  <p className="postContent__username">{postData.username}</p>
                </Link>
              </div>

              <div className="postComments" />
              <div className="postInfo">
                <div className="postActions">
                  {liked ? (
                    <Tooltip title="Unlike post">
                      <Icon
                        type="heart"
                        className="likePost"
                        theme="filled"
                        style={{ color: "#d41c00" }}
                        onClick={() => this.props.unlikePost(postData.id)}
                      />
                    </Tooltip>
                  ) : (
                    <Tooltip title="Like post">
                      <Icon
                        type="heart"
                        className="likePost"
                        onClick={() => this.props.likePost(postData.id)}
                      />
                    </Tooltip>
                  )}
                  <Tooltip title="Comment">
                    <Icon type="message" className="commentButton" />
                  </Tooltip>
                </div>
                <Tooltip title="Refresh comments">
                  <Icon type="reload" className="reloadComments" />
                </Tooltip>
              </div>
              <div />
            </div>
          </div>
        )}
      </div>
    );
  }
}

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

const mapDispatchToProps = dispatch => {
  return {
    getPostData: postId => dispatch(getPostData(postId)),
    likePost: postId => dispatch(likePost(postId)),
    unlikePost: postId => dispatch(unlikePost(postId))
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(PostDetails);

postsReducer.js

const initialState = {
  creatingPost: false,
  feed: [],
  createdPost: false,
  feedUpdated: false,
  postDetails: {}
};

const postsReducer = (state = initialState, action) => {
  switch (action.type) {
    case "CREATING_POST":
      return {
        ...state,
        creatingPost: true,
        createdPost: false
      };
    case "ADD_POST":
      return {
        ...state,
        feed: state.feed.concat(action.payload)
      };
    case "FETCH_FEED":
      return {
        ...state,
        feed: action.payload
      };
    case "CREATED_POST":
      return {
        ...state,
        creatingPost: false,
        createdPost: true
      };
    case "UPDATE_FEED":
      return {
        ...state,
        feed: action.payload,
        feedUpdated: true
      };
    case "GET_POST_DATA":
      return {
        ...state,
        postDetails: action.payload
      };
    case "RESET_FEED_UPDATED":
      return {
        ...state,
        feedUpdated: false
      };
    case "RESET_CREATED_POST":
      return {
        ...state,
        createdPost: false
      };
    case "LIKED_POST":
      const { postDetails } = state;

      postDetails.liked = true;
      postDetails.likes += 1;
      return {
        ...state,
        postDetails: postDetails
      };

    case "UNLIKED_POST":
      const postDetails1 = state.postDetails;

      postDetails1.liked = false;
      postDetails1.likes -= 1;

      return {
        ...state,
        postDetails: postDetails1
      };
    case "CLEAR_POST_DATA":
      return initialState;
    default:
      return state;
  }
};

export default postsReducer;

postsActions.js

import Axios from "axios";
import moment from "moment";
import store from "../store";
export const createPost = postData => {
  return (dispatch, getState) => {
    dispatch({ type: "CREATING_POST" });

    Axios.post("/api/post/new", {
      imageUrl: postData.imageUrl,
      caption: postData.caption
    })
      .then(res => {
        dispatch({ type: "CREATED_POST" });

        dispatch({ type: "ADD_POST", payload: res.data.post });
      })
      .catch(err => {
        console.log(err);
      });
  };
};

export const fetchFeed = () => {
  return (dispatch, getState) => {
    Axios.get("/api/user/feed")
      .then(res => {
        var feed = res.data.feed;
        const state = store.getState();
        const likedPosts = state.user.userData.likedPosts;

        for (var i = 0; i < feed.length; i++) {
          for (var j = 0; j < feed.length - i - 1; j++) {
            if (moment(feed[j + 1].createdAt).isAfter(feed[j].createdAt)) {
              var temp = feed[j];
              feed[j] = feed[j + 1];
              feed[j + 1] = temp;
            }
          }
        }

        for (var i = 0; i < feed.length; i++) {
          if (likedPosts.indexOf(feed[i]._id) > -1) {
            feed[i]["liked"] = true;
          } else {
            feed[i]["liked"] = false;
          }
        }

        console.log(feed);
        dispatch({ type: "FETCH_FEED", payload: feed });
      })
      .catch(err => {
        console.log(err);
      });
  };
};

export const likePost = postId => {
  return (dispatch, getState) => {
    Axios.put("/api/post/like", { postId: postId })
      .then(res => {
        const feed = store.getState().posts.feed;

        feed.forEach(post => {
          if (post._id === postId) {
            post.liked = true;
          }
        });

        dispatch({ type: "UPDATE_FEED", payload: feed });
        dispatch({ type: "LIKED_POST", payload: res.data.postId });
      })
      .catch(err => {
        console.log(err);
      });
  };
};

export const unlikePost = postId => {
  return (dispatch, getState) => {
    Axios.put("/api/post/unlike", { postId: postId })
      .then(res => {
        const feed = store.getState().posts.feed;

        feed.forEach(post => {
          if (post._id === postId) {
            post.liked = false;
          }
        });

        dispatch({ type: "UPDATE_FEED", payload: feed });
        dispatch({ type: "UNLIKED_POST", payload: res.data.postId });
      })
      .catch(err => {
        console.log(err);
      });
  };
};

export const getPostData = postId => {
  return (dispatch, getState) => {
    Axios.get(`/api/post/${postId}`)
      .then(res => {
        const likedPosts = store.getState().user.userData.likedPosts;

        if (likedPosts.indexOf(postId) > -1) {
          res.data.post["liked"] = true;
        } else {
          res.data.post["liked"] = false;
        }

        dispatch({ type: "GET_POST_DATA", payload: res.data.post });
      })
      .catch(err => {
        console.log(err);
      });
  };
};

export const resetFeedUpdated = () => {
  return (dispatch, getState) => {
    dispatch({ type: "RESET_FEED_UPDATED" });
  };
};

export const resetCreatedPost = () => {
  return (dispatch, getState) => {
    dispatch({ type: "RESET_CREATED_POST" });
  };
};

Upvotes: 3

Views: 1478

Answers (3)

G_S
G_S

Reputation: 7110

When working with Redux, never forget the three principles

  • Single Source of truth
  • State is ready only
  • Reducers must be pure functions: Reducers take previous state and some action and modifies it and returns new state. We should never mutate state. We should create new objects and return them.

You have mutated existing state in your reducer functions. This doesnt trigger componentdidupdate because, connect method ( it checks mapStateToProps) treats that there is nothing that changed (It checks reference and since reference didnt change Component is not invoked).

You can use Object.assign or use spread operator which helps to make your reducers return a new object.

Change your Liked and unlinked posts reducer functions to return a new object instead of mutating existing object.

@azundo added how your code should be to achieve what you need.

Upvotes: 0

Auskennfuchs
Auskennfuchs

Reputation: 1737

You should check, if the comparison if (prevProps.postDetails !== this.props.postDetails) ever hits. Because with the like function you only change properties of the same object, the comparison will fail, because it's still the same object reference for postDetails. Try to return a new object in your reducer:

case "LIKED_POST":
      const { postDetails } = state;

      postDetails.liked = true;
      postDetails.likes += 1;
      return {
        ...state,
        postDetails: {
           ...postDetails
        },
      }

Also if you're not changing anything of the object inside the component but in Redux store why not use the component property directly? You can remove the state object and the componentDidUpdate. Also you could refactor it to a function component.

  render() {
    const { postDetails: postData } = this.props;
    ...
  }

Upvotes: 1

azundo
azundo

Reputation: 6052

Your LIKED_POST and UNLIKED_POST reducer cases are not pure - they are are mutating the existing postDetails object in the state and putting it back into state so connect is optimizing and not re-rendering when it does a shallow equals comparison on postDetails from the previous and next props in componentShouldUpdate. Make sure you're creating a completely new value for postDetails like:

    case "LIKED_POST":
      const { postDetails } = state;

      const newPostDetails = {
        ...postDetails,
        liked: true,
        likes: postDetails.likes + 1,
      };
      return {
        ...state,
        postDetails: newPostDetails
      };

Upvotes: 2

Related Questions