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:
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.
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:
deleteComment()
, you referencethis.props.post
. That object is presumably extracted from the store viamapStateToProps
, so it's the object reference in the store.post
gets passed tothis.props.deleteComments(post)
deleteComments()
dispatches({type : DELETE_COMMENTS, post})
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 thepost
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 thestate
object and update it with the new post object. Based off what the rest of your code does, my guess is this should work: