Reputation: 1076
In the code below, if I only return a topic
object from mapStateToProps
, the SingleTopic
component is not re-rendered when the state updates. But if I return { topic, state }
from it, the component is re-rendered.
import React from 'react';
import { connect } from 'react-redux';
import SingleTopic from '../components/SingleTopic';
import { incrementTopicRating, decrementTopicRating } from '../actions/forum';
let mapStateToProps = (state, ownProps) => {
let topic = state.filter((t) => {
return +t.id === +ownProps.params.id;
})[0];
return { topic };
};
let mapDispatchToProps = (dispatch) => {
return {
onUpvote: (id) => { dispatch(incrementTopicRating(id)); },
onDownvote: (id) => { dispatch(decrementTopicRating(id)); }
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SingleTopic);
I might be doing something wrong, but as far as I understand, the component passed into connect
is supposed to be updated automatically when the state updates. Could someone explain me where is my mistake?
Reducer:
export default (state = [], action) => {
switch (action.type) {
case 'ADD_TOPIC': return addTopic(state, action);
case 'INCREMENT_TOPIC_RATING': return incrementTopicRating(state, action);
case 'DECREMENT_TOPIC_RATING': return decrementTopicRating(state, action);
default: return state;
}
};
const addTopic = (state, action) => {
let { title, body, id, rating } = action;
let newTopic = { title, body, id, rating };
return [newTopic, ...state];
};
const incrementTopicRating = (state, action) => {
return state.map((topic) => {
if (+topic.id === +action.id) {
topic.rating += 1;
return topic;
}
return topic;
});
};
const decrementTopicRating = (state, action) => {
return state.map((topic) => {
if (+topic.id === +action.id) {
topic.rating -= 1;
return topic;
}
return topic;
});
};
UPD
The SingleTopic
component is rendered if I add a second property to the returned object. And this property must be of type array or object, like this:
let foo = [];
return { topic, foo };
Upvotes: 4
Views: 139
Reputation: 989
Your were mutating the state in your reducer's incrementTopicRating
and decrementTopicRating
methods.
Here is the updated code for your recuder:
export default (state = [], action) => {
switch (action.type) {
case 'ADD_TOPIC': return addTopic(state, action);
case 'INCREMENT_TOPIC_RATING': return incrementTopicRating(state, action);
case 'DECREMENT_TOPIC_RATING': return decrementTopicRating(state, action);
default: return state;
}
};
const addTopic = (state, action) => {
let { title, body, id, rating } = action;
let newTopic = { title, body, id, rating };
return [newTopic, ...state];
};
const incrementTopicRating = (state, action) => {
const newState = state.map((topic) => {
if (+topic.id === +action.id) {
return Object.assign({}, topic, { rating: topic.rating+1 });
}
return topic;
});
return newState;
};
const decrementTopicRating = (state, action) => {
return state.map((topic) => {
if (+topic.id === +action.id) {
return Object.assign({}, topic, { rating: topic.rating-1 });
}
return topic;
});
};
Here is a JSBIN with the corrected solution: https://jsbin.com/nihabaqumo
In order to avoid this kind of issues in the future, consider using a library that allows you to generate immutable data like the facebook Immutable one. Use this kind of library to create a state that cannot be changed once created.
MORE INFO ADDED
In answer to the question:
If the map
method returns a new array
and is changing the local topic
variable then I didn't change the previous state, so where is the problem? As I see it incrementTopicRating
doesn't change previous state and returns a new one. Could you clarify it please?
In your reducer's code the map
method does return a new array
but with the same object
references.
When react-redux
checks for a change to define if the component SingleTopic
should update (be re-rendered) it does a shallow equal check of the new props versus the previous props. Since both the old and new arrays contain references to the same objects, the shallow equal returns true for all object comparisons in the old and new arrays even for the one for which the rating had been modified. The SingleTopic
component is then not aware of a change and is never re-rendered.
More information can be found here: http://rackt.org/redux/docs/Troubleshooting.html
Also look at the Connect component source code, more specifically the shouldComponentUpdate
method which determines if a component should be updated/re-rendered or not: https://github.com/rackt/react-redux/blob/04693ca0cabe021b27b62eb9240b51459ffe8c32/src/components/connect.js
Upvotes: 4
Reputation: 30671
I think the problem is caused because your reducers mutate the items:
if (+topic.id === +action.id) {
topic.rating -= 1;
return topic;
}
Try creating a new item instead:
if (+topic.id === +action.id) {
return { ...topic, rating: topic.rating - 1 };
}
Upvotes: 1