Ways to fix a child component that is not re-rendering (due to change in data being passed in as props, not state)?


Background

I am working on a Meteor app that uses ReactJS as the rendering library.

Currently, I'm having trouble with having a Child component re-render when data has been updated, even though the parent is accessing the updated data & supposedly passing it down to the child.

The Parent component is a Table of Data. The Child component is a Click-to-edit date field.

The way it (theoretically) works: Parent component passes the existing data for date into child component as a prop. Child component takes that existing props data, handles it & sets some states using it and then has 2 options:

  • default: display data
  • if user clicks on data field: change to input & allow user to select date (using react-datepicker), changing the state--when user clicks outside of field, triggers a return to display only & saves the updated data from state to database

I am using the child component twice per row in the table, and each time it is used, it needs access to the most current date data from the database. So if it the data is changed in one field, the second field should reflect that change.

Problem

The second field is not reflecting changes in the database unless I manually refresh the page and force the child component to render with the new data. The edited field is reflecting the data change, because it's reflecting what's stored in state.

After reading React documentation, I am sure the problem is because the date is coming in as a prop, then being handled as a state--and because the component isn't going to re-render from a prop change.

My Question

What do I do to fix this?

All of the reading I've done of the docs has strongly recommended staying away from things like forceUpdate() and getDerivedStateFromProps(), but as a result I'm not sure how to have data pass through the way that I want it to.

Thoughts?

My Code

I've abbreviated the code a little bit & removed variable names specific to my project, but I can provide more of the actual if it'll help. I think my question is more conceptual than it is straight up debugging.

Parent

ParentComponent () {
    //Date comes as a prop from database subscription
    var date1 = this.props.dates.date1 
    var date2 = this.props.dates.date2
    return(
        <ChildComponent 
            id='handle-date-1'
            selected={[date1, date2]} />
        <ChildComponent 
            id='handle-date-2'
            selected={[date1, date2]} />
    )
}

Child

ChildComponent() {
    constructor(props) {
        super(props);
        this.state = {
            date1: this.props.selected[0],
            date2: this.props.selected[1],
            field: false,
        };
    }

    handleDatePick() {
        //Handles the event listeners for clicks in/out of the div, handles calling Meteor to update database.
    }
    renderDate1() {
        return(
            <div>
                {this.state.field == false &&
                    <p onClick={this.handleClick}>{formatDate(this.state.date1)}</p>}
                {this.state.field == true &&
                    <DatePicker
                        selected={this.state.date1}
                        startDate={this.state.date1}
                        onChange={this.handleDatePick}
                    />
                }
            </div>
        )
    }
    renderDate2() {
        return(
            <div>
                {this.state.field == false &&
                    <p onClick={this.handleClick}>{formatDate(this.state.date2)}</p>}
                {this.state.field == true &&
                    <DatePicker
                        selected={this.state.date2}
                        startDate={this.state.date2}
                        onChange={this.handleDatePick}
                    />
                }
            </div>
        )
    }
    render() {
        return(
            //conditionally calls renderDate1 OR renderDate2
        )
    }
}

(If this code/my explanation is rough, it's because I'm still a fairly beginner/low level developer. I don't have formal training, so I'm learning on the job while working on a pretty difficult app. As a solo developer. It's a long story. Please be gentle!)

The React docs have a section on best practice usage of constructor(). Reading that, paying close attention to the yellow-highlighted "Note" section, should illuminate the exact problem you're running into.

Essentially, constructor() is only run once, and is most commonly used to initialize internal/local state (or bind methods). This means that your child component is setting date1 and date2 with the values from your selected prop once when constructor() is called for that child. Whatever the value of selected is at the time constructor() is called will be set to the child's state, and will remain the same even if the value continues to change.

Thus, any successive updates to the selected prop passed to a child component will not be reflected in that component's internal state, meaning there will be no re-render of that component. You'll need to make use of React's setState() method elsewhere in your child component to properly update that child's state and trigger a re-render.

Using a combination of the React lifecycle methods to properly update your child components is the way to go. The snippet below gives you the main idea of what to change about your implementation in componentDidMount() and componentDidUpdate()

class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      date1: 0,
      date2: 0,
    };
  }

  /*
    Any time the child mounts to the DOM, 
    you can use the method below to set 
    your state using the current value of your 
    selected prop from the parent component...
  */

  componentDidMount() {
    this.setState({
      date1: this.props.selected[0],
      date2: this.props.selected[1]
    });
  }

  /* 
   When the child receives an update from its parent, 
   in this case a new date selection, and should 
   re-render based on that update, use the method below 
   to make a comparison of your selected prop and then 
   call setState again...
  */

  componentDidUpdate(prevProps) {
    if (prevProps.selected !== this.props.selected) {
      this.setState({
        date1: this.props.selected[0],
        date2: this.props.selected[1]
      });
    }
  }

  render() {
    const { date1, date2 } = this.state;
    return (
      <div style={{ border: `4px dotted red`, margin: 8, padding: 4 }}>
        <h1 style={{ fontWeight: 'bold' }}>Child</h1>
        <h2>The value of date1 is {date1}</h2>
        <h2>The value of date2 is {date2}</h2>
      </div>
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      valOne: 0,
      valTwo: 0
    };
  }

  incrementOne = () => {
    this.setState(prevState => ({ valOne: (prevState.valOne += 1) }));
  };

  incrementTwo = () => {
    this.setState(prevState => ({ valTwo: (prevState.valTwo += 1) }));
  };

  render() {
    const { valOne, valTwo } = this.state;
    return (
      <div style={{ border: `4px solid blue`, margin: 8, padding: 4 }}>
        <h1 style={{ fontWeight: 'bold', fontSize: 18 }}>Parent</h1>
        <button onClick={() => this.incrementOne()}>Increment date1</button>
        <button onClick={() => this.incrementTwo()}>Increment date2</button>
        <Child selected={[valOne, valTwo]} />
      </div>
    );
  }
}

ReactDOM.render(<Parent />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

Since you're using the React Component pattern for your children, making good use of the React lifecyle methods will really help you out here. I can't suggest strongly enough that you learn the React component lifecyle. As you continue to use React, it will become essential in cases such as this. componentDidMount() and componentDidUpdate() are good places to start.

Hope that this helps. Let us know how it turns out.