React-redux: Get over shallow comparison issues?

Created on 23 Feb 2016  路  11Comments  路  Source: reduxjs/react-redux

I was working on an issue today that I wasn't able to explain in other way than shallow comparison.

Essentially I have a state similar to following:

{
 meta: {},
 contents: [
   { content_type: 1, text:"" },
   { content_type: 2, text:"" },
   { content_type: 1, text:"" }
 ]
}

there are many content types, this is just a simple example. What I am doing is re ordering these in state to cause re ordering on ui side, ie I can move first object from contents array down or second one up just fine, but when it comes to re ordering 2 object with same content type, it successfully updates the redux state, however react doesn't re-render the ui, and I came to conclusion that this could be caused by shallow comparison, as objects that switched places are very very similar, only their text fields are different.

I am looking for a way to work around this.

following is how I update my redux state:

...
case STORY_ACTIONS.UPDATE_STORY_CONTENTS:
      return { ...state, contents: action.payload }
...

where action.payload is correctly re-ordered contents array, I don't think this is an issue, as it does update redux state correctly.

Most helpful comment

@Sunakujira1 I got it! so I was setting key of each storyBlock to index gathered from map() somehow this led to isues, changing this key to something like uniqe random id I generated for each block helps.

All 11 comments

Update: I started adding random number field as additional parameter so:

{ content_type: 1, text:"", order: random_number }

Still no luck, so I am not so sure now if it is due to shallow comparising, hence am open for suggestions to what can be causing this.

Can you show the component code?

This is a very stripped down version of it, that shows all necessary bits:

(I believe that it is connected correctly as each block is updated correctly, I only experience issues when to similar ones are next to each other)

// -- Dependencies -------------------------------------------------------------
import React, { Component } from 'react';
import { connect } from 'react-redux';



...


// -- Class --------------------------------------------------------------------
class StoryEditor extends Component {

  //Render story block
  renderStoryContent(content, index, array) {
    switch(content.content_type) {
      // Media block
      case 1:
        return (
          ...
        );
      // Text block
      case 2:
        return (
        ...
        );
      default:
        console.warn("Unable to proccess following content ", content);
    }
  }

  render() {

    // Handle existing story
    return (
      <div className="story-drag-edditor">
        { this.props.story.contents.map(this.renderStoryContent) }
      </div>
    );
  }
}



// -- Promote component to container -------------------------------------------
function mapStateToProps(state) {
  return { story: state.story }
}



// -- Export -------------------------------------------------------------------
export default connect(mapStateToProps)(StoryEditor);

Where are the parts that don鈥檛 update? Are they in child components? Do those components use local React state?

Alright, sorry for delayed response, but here goes:

The switch statement you can see above within renderStoryContent function is responsible for rendering StoryBlock components that have another blocks inside that are content_type specific, so example of text block (one of which has the issue I am talking about) would look like

case 2:
  return (
     <StoryBlock
            key={index}
            order={index}
            type={CONTENT_TYPES.TEXT}
            title="text">
            <StoryBlockText
              order={index}
              ids={ content.ids ? content.ids : [] }
              content={ content.content_body.text.html ? content.content_body.text.html : ""} />
          </StoryBlock>
             );

And this is how StoryBlock component looks:

// -- Dependencies -------------------------------------------------------------
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';



// -- App ----------------------------------------------------------------------
import TextButton from '../App/text-button';
import {
  sortStoryBlockDown,
  sortStoryBlockUp,
  removeStoryBlock,
  duplicateStoryBlock }
from '../../actions/Story/story';



// -- Class --------------------------------------------------------------------
class StoryBlock extends Component {
  constructor(props) {
    super(props);
    this.state = { parentContent: "" };
  }

  componentWillUnmount() {
    this.parentContent = "";
  }

  //Pass child contents to parent
  updateBlockContent(childContent) {
    this.parentContent = childContent;
  }

  //Render
  render() {

    //Create children with props
    var childrenWithProps = React.cloneElement(this.props.children, { updateBlockContent: this.updateBlockContent.bind(this) });

    return (
      <section className={`story-block ${this.props.type}`}>
        <div className="story-block-header clearfix">
          <div className="pull-left"> {this.props.title} </div>
          <div className="pull-right">
            <TextButton
              helpers="uppercase"
              onClick={ () => this.props.duplicateStoryBlock(this.props.story, this.props.order, this.props.type, this.parentContent) }>
              Duplicate {}
            </TextButton>
            <TextButton
              helpers="uppercase"
              onClick={ () => this.props.removeStoryBlock(this.props.story, this.props.order, this.props.type) }>
              Remove
            </TextButton>
            <span
              className="sort-down"
              onClick={ () => this.props.sortStoryBlockDown(this.props.story, this.props.order, this.props.type, this.parentContent) }>
            </span>
            <span
              className="sort-up"
              onClick={ () => this.props.sortStoryBlockUp(this.props.story, this.props.order, this.props.type, this.parentContent) }>
            </span>
          </div>
        </div>

        <div className="story-block-main">
          {childrenWithProps}
        </div>

        <div className="story-block-drag-indicator">
          <span>Place here</span>
        </div>
      </section>
    );
  }
}



// -- Promote component to container -------------------------------------------
function mapStateToProps(state) {
  return { story: state.story }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    sortStoryBlockDown: sortStoryBlockDown,
    sortStoryBlockUp: sortStoryBlockUp,
    removeStoryBlock: removeStoryBlock,
    duplicateStoryBlock: duplicateStoryBlock
  }, dispatch);
}



// -- Export -------------------------------------------------------------------
export default
  connect(mapStateToProps, mapDispatchToProps)(StoryBlock);

Similarly, if you need it, here is StoryBlockText component (This one uses react-medium-editor plugin, but I don't think it is an issue, as I get similar bug on another component that doesn't require it, however both of these components have elements with contentEditable) :

// -- Dependencies -------------------------------------------------------------
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Editor from 'react-medium-editor';



// -- App ----------------------------------------------------------------------
import { updateText } from '../../actions/Story/story-block-text.js';



// -- Helpers ------------------------------------------------------------------
const editorOptions =  {
  placeholder: { text: 'Start typing yo.' },
  anchor: {placeholderText: 'Paste link and press enter.'},
  toolbar: {
    buttons: [
      {name: 'bold', contentDefault: 'Bold'},
      {name: 'italic', contentDefault: 'Italic'},
      {name: 'underline', contentDefault: 'Underline'},
      'h1',
      'h2',
      {name: 'orderedlist', contentDefault: '1. List'},
      {name: 'unorderedlist', contentDefault: '&bullet; List'},
      {name: 'anchor', contentDefault: 'Link'}
    ]
  }
}



// -- Class --------------------------------------------------------------------
class StoryBlockText extends Component {
  constructor(props) {
    super(props)
    this.state = {text: this.props.content}
  }

  componentDidMount() {
    this.props.updateBlockContent(this.props.content);
  }

  //Handle on change
  handleOnChange(text, medium) {
    this.setState({text: text});
    this.props.updateBlockContent(this.state.text);
  }

  //Render
  render() {
    return (
      <div className="story-block-text" ref={`textBlock${this.props.order}`}>
        <Editor
          text={ this.state.text }
          onChange={ this.handleOnChange.bind(this) }
          options={ editorOptions } />
      </div>
    );
  }
}



// -- Promote component to container -------------------------------------------
function mapStateToProps(state) {
  return { story: state.story }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    updateText: updateText
  }, dispatch);
}



// -- Export -------------------------------------------------------------------
export default StoryBlockText;

By any chance, are you mutating the state to re-order the contents?

@Sunakujira1 I don't think so, you can see from my first comment how my states look and how I update its contents. It works fine in majority of cases as well, only when two neigbour contents with similar type are sorted it doesn't. I'm starting to think that this could be something to do with contentEditable as all elements that have an issue have it within them.

@IljaDaderko Hmm, you are right, state seems fine.
Do you have key specified for each StoryBlock? Seems to be quite important as specified here.
Also, I am not as familiar with this.props.children, but I'm guessing it may be the case that StoryBlockText are not being re-rendered due to the StoryBlock not having any difference in props.

@Sunakujira1 I got it! so I was setting key of each storyBlock to index gathered from map() somehow this led to isues, changing this key to something like uniqe random id I generated for each block helps.

This is why I asked about the local state. If you don鈥檛 specify the key, components will receive each other鈥檚 props on reorder. Which makes reorder slower but that is not the root of the problem.

The root of the problem is you derive local state from initial props in the constructor. But props may be received by the component more than once. When your components receive new props during reorder, you forget to reset the state to reflect those new values.

To do it, you would need to implement componentWillReceiveProps(nextProps) that calls setState if nextProps.text !== this.props.text. Adding a key also helps but it heals the symptom rather than the actual cause of the problem.

Hi @gaearon , thanks for your answer. I am having a similar problem (check http://stackoverflow.com/questions/39513753/how-can-i-force-re-rendering-when-redux-state-is-changed-but-react-does-not-trig).

As far as I understand, React will re-render a component if nextProps and actual props differ. Why is it then needed to call the componentWillReceiveProps(nextProps) method, should the component do this automatically?

Thanks again!

Was this page helpful?
0 / 5 - 0 ratings