javascriptreactjs

React component not re-rendering on state change


I have a React Class that's going to an API to get content. I've confirmed the data is coming back, but it's not re-rendering:

var DealsList = React.createClass({
  getInitialState: function() {
    return { deals: [] };
  },
  componentDidMount: function() {
    this.loadDealsFromServer();
  },
  loadDealsFromServer: function() {
    var newDeals = [];

    chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
      newDeals = deals;
    });

    this.setState({ deals: newDeals });
  },
  render: function() {
    var dealNodes = this.state.deals.map(function(deal, index) {
      return (
        <Deal deal={deal} key={index} />
      );
    });
    return (
      <div className="deals">
        <table>
          <thead>
            <tr>
              <td>Name</td>
              <td>Amount</td>
              <td>Stage</td>
              <td>Probability</td>
              <td>Status</td>
              <td>Exp. Close</td>
            </tr>
          </thead>
          <tbody>
            {dealNodes}
          </tbody>
        </table>
      </div>
    );
  }
});

However, if I add a debugger like below, newDeals are populated, and then once I continue, i see the data:

  loadDealsFromServer: function() {
    var newDeals = [];

    chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
      newDeals = deals;
    });
    debugger
    this.setState({ deals: newDeals });
  },

This is what's calling deals list:

var Gmail = React.createClass({
  render: function() {
    return (
      <div className="main">
        <div className="panel">
          <DealsList person={this.props.person} />
        </div>
      </div>
    );
  }
});

Solution

  • That's because the response from chrome.runtime.sendMessage is asynchronous; here's the order of operations:

    var newDeals = [];
    
    // (1) first chrome.runtime.sendMessage is called, and *registers a callback*
    // so that when the data comes back *in the future*
    // the function will be called
    chrome.runtime.sendMessage({...}, function(deals) {
      // (3) sometime in the future, this function runs,
      // but it's too late
      newDeals = deals;
    });
    
    // (2) this is called immediately, `newDeals` is an empty array
    this.setState({ deals: newDeals });
    

    When you pause the script with the debugger, you're giving the extension time to call the callback; by the time you continue, the data has arrived and it appears to work.

    To fix, you want to do the setState call after the data comes back from the Chrome extension:

    var newDeals = [];
    
    // (1) first chrome.runtime.sendMessage is called, and *registers a callback*
    // so that when the data comes back *in the future*
    // the function will be called
    chrome.runtime.sendMessage({...}, function(deals) {
      // (2) sometime in the future, this function runs
      newDeals = deals;
    
      // (3) now you can call `setState` with the data
      this.setState({ deals: newDeals });
    }.bind(this)); // Don't forget to bind(this) (or use an arrow function)
    

    [Edit]

    If this doesn't work for you, check out the other answers on this question, which explain other reasons your component might not be updating.