Sergey
Sergey

Reputation: 1076

A component passed into connect() is not rendered

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

Answers (2)

DDA
DDA

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

Atanas Korchev
Atanas Korchev

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

Related Questions