State is returning new state without touching the

2019-08-18 11:24发布

Before I start posting the code I will explain what I want to happen and what is happening.

I have been working with this all day and finally got something to work. The problem is that I have no clue why it works and if it's the best way to do it. What I want to happen is when a User clicks delete on a comment I want the comment to be delete from the current post on the back end and from the state on the front end.

UPDATE: I did not really add enough to show what I was talking about. When a user clicks the delete button it will fire off this function here:

deleteComment(comment) {
    const {id} = this.props.match.params;
    const {user, post, auth} = this.props;

    if(!user) {
      return (<div></div>);
    }

    if(auth) {
      if(user._id === comment.author.id){
        this.props.deleteComments(post, comment._id, () => {
          this.props.history.push(`/posts/${post._id}`);
        });
      }
    }
  }

which then calls the action:

export function deleteComments(post, comment_id, cb) {
  return function(dispatch) {
    axios.delete(`${ROOT_URL}/${post._id}/comments/${comment_id}`)
      .then(() => {
        dispatch({
          type: DELETE_COMMENTS,
          payload: comment_id,
          post: post
        });
      })
      .then(() => cb())
      .catch((error) => {
        console.log(error);
      });
  }
}

What I am wondering: Really all the code above works so it is more of a reference than anything. I am wondering why in the reducer when DELETE_COMMENTS is caught by the switch case how the current state which is below in the picture is changed to a new state without the comment I deleted. I don't explicitly ever edit the state or change it so how does return (state); actually change it here?

To elaborate more my current state looks as so:

enter image description here

As you can see I have a Post with comments inside of it. The comments are stored inside an array and each of them are objects with text, author.id, author.email, and createdAt values. Now as you can see in the reducer_post when the action is fired off it sends the DELETE_COMMENTS dispatch type off and the state is changed.

For help: action.payload is the comment._id, and action.post is the current post that is being viewed.

import {
  GET_ALL_POSTS,
  GET_POST,
  CREATE_POST,
  DELETE_POST,
  UPDATE_POST,
  DELETE_COMMENTS
} from '../actions/types';
import _ from 'lodash';

export default function(state = {}, action) {
  switch(action.type) {
    case GET_ALL_POSTS:
      return _.mapKeys(action.payload.data, '_id');
    break;
    case GET_POST:
      return {...state, [action.payload.data._id]: action.payload.data};
    break;
    case DELETE_POST:
      return _.omit(state, action.payload);
    break;
    case DELETE_COMMENTS:
      let newCommentArray = _.reject(action.post.comments, {'_id': action.payload});
      let newPost = action.post;
      newPost.comments = newCommentArray;
      return (state);
    case UPDATE_POST:
      let updates = {[action.payload.data._id]: action.payload.data};
      return _.merge(...state, updates);
    break;
    default:
      return state;
    break;
  }
}

I know the back-end works just fine and this also works just fine. My question is how does it update the state with me just putting return (state);. I did not have to do something like return _.merge(...state, newPost); or something like that. How is all of this working just fine, and no I did not just copy some tutorial and come on here and ask this just to figure out how someone else did it. At the moment I believe in magic so it would be nice for an explanation.

Also since we are here, is this the best way to do it? Or is there a better more clean way of doing things here.

1条回答
够拽才男人
2楼-- · 2019-08-18 12:10

I think you're actually directly mutating the state, and somehow getting away with it in the sense that the component is still updating.

Let's follow the trail of logic:

  1. In deleteComment(), you reference this.props.post. That object is presumably extracted from the store via mapStateToProps, so it's the object reference in the store.
  2. post gets passed to this.props.deleteComments(post)
  3. deleteComments() dispatches ({type : DELETE_COMMENTS, post})
  4. In the reducer, you have let newPost = action.post; newPost.comments = newCommentArray. But, at that point, newPost still points to the exact same object that's already in the store state. So, you've directly mutated it.

Normally, this kind of mutation would result in your component not re-rendering, so I can only assume that some other value extracted in mapState is also getting updated properly, and thus causing a re-render.

As discussed in the Structuring Reducers - Immutable Update Patterns page in the Redux docs, let someVariable = anotherVariable doesn't create a new object reference - it merely creates a second variable that points to the same object reference. Proper immutable updates require copies of every level of nesting that you're working with, not just the one level. In your case, you're creating a new array via _.reject(), but you're not creating a copy of the post object.

So, in the end, your reducer logic is incorrect, and your application is sorta kinda working by accident :)

update

Responding to your question about a proper update: you need to create a copy of the post object and update it with the new comments array, and you need to copy the state object and update it with the new post object. Based off what the rest of your code does, my guess is this should work:

case DELETE_COMMENTS:
     const newCommentArray = _.reject(action.post.comments, {'_id': action.payload});

     const newState = {
         ...state,
         [action.post.id] : {
             ...action.post,
             comments : newCommentArray
         }
     };

     return newState;
查看更多
登录 后发表回答