94

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>
    );
  }
});
brandonhilkert
  • 4,205
  • 5
  • 25
  • 38

12 Answers12

132

My scenario was a little different. And I think that many newbies like me would be stumped - so sharing here.

My state variable is an array of JSON objects being managed with useState as below:

const [toCompare, setToCompare] = useState([]);

However when update the toCompare with setToCompare as in the below function - the re-render won't fire. And moving it to a different component didn't work either. Only when some other event would fire re-render - did the updated list show up.

const addUniversityToCompare = async(chiptoadd) =>
  {
      var currentToCompare = toCompare;
      currentToCompare.push(chiptoadd);
      setToCompare(currentToCompare);
  }

This was the solution for me. Basically - assigning the array was copying the reference - and react wouldn't see that as a change - since the ref to the array isn't being changed - only content within it. So in the below code - just copied the array using slice - without any change - and assigned it back after mods. Works perfectly fine.

const addUniversityToCompare = async (chiptoadd) => {
    var currentToCompare = toCompare.slice();
    currentToCompare.push(chiptoadd);
    setToCompare(currentToCompare);
}

Hope it helps someone like me. Anybody, please let me know if you feel I am wrong - or there is some other approach.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
KManish
  • 1,551
  • 2
  • 11
  • 7
112

I'd like to add to this the enormously simple, but oh so easily made mistake of writing:

this.state.something = 'changed';

... and then not understanding why it's not rendering and Googling and coming on this page, only to realize that you should have written:

this.setState({something: 'changed'});

React only triggers a re-render if you use setState to update the state.

Stijn de Witt
  • 40,192
  • 13
  • 79
  • 80
  • 1
    This is the exact issue I was having. It's strange that they don't throw a warning since they throw one when trying to update props. – AndrewJM Dec 13 '15 at 22:38
  • 3
    @AndrewJM They can't throw a warning. They could if you'd write `this.state = 'something'` because you would be hitting the setter for `state`, but in the example above, the code hits the getter, which returns an object and it then ends up setting a field on an object that is only a copy of the state. – Stijn de Witt Jun 23 '17 at 21:37
  • I have this issue, I update the state once with setState in the end of the Promise Chain. Everything is properly bound and using arrow functions. I'm getting crazy over here – TheBigCheese Nov 12 '19 at 15:51
  • 1
    @TheBigCheese Better write a (good) question on it. There are people that can help, but not here in the comments of another question. – Stijn de Witt Nov 13 '19 at 19:17
  • 2
    @StijndeWitt I logged in to Stackoverflow to upvote your answer. Thanks bro ;D – Esterlinkof May 20 '20 at 22:10
  • 1
    Very Important concept for beginners to understand and remember. – invinciblemuffi Jul 29 '21 at 10:34
  • it's not working for me, I have both functional components and Class-Based Components and I'm using useState hook for functional one and this.setState for class based, but in neither case my components do not re-render :( , and I should mention this : my component's states aren't changing inside of component itself, instead I am passing the handler to change state using props to their children, But I'm sure the handler is getting called and is running ```this.setState``` and is changing the state... – Mahdiar Mransouri Apr 30 '22 at 20:40
29

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.

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • Duh! I should've known this was the case when the debugger line fixed it. The bind(this) was the thing I missed when I first attempted this. Thanks, great detailed comment! – brandonhilkert Sep 19 '14 at 15:54
19

Another oh-so-easy mistake, which was the source of the problem for me: I’d written my own shouldComponentUpdate method, which didn’t check the new state change I’d added.

Lynn
  • 10,425
  • 43
  • 75
15

To update properly the state, you shouldn't mutate the array. You need to create a copy of the array and then set the state with the copied array.

const [deals, setDeals] = useState([]);
    
   function updateDeals(deal) {
      const newDeals = [...deals]; // spreading operator which doesn't mutate the array and returns new array
      newDeals.push(deal);

      // const newDeals = deals.concat(deal); // concat merges the passed value to the array and return a new array
      // const newDeals = [...deals, deal] // directly passing the new value and we don't need to use push
    
      setDeals(newDeals);
    }
Tsanev96
  • 151
  • 1
  • 3
10

In my case, I was calling this.setState({}) correctly, but I my function wasn't bound to this, so it wasn't working. Adding .bind(this) to the function call or doing this.foo = this.foo.bind(this) in the constructor fixed it.

derf26
  • 862
  • 1
  • 7
  • 16
3

My issue was that I was using 'React.PureComponent' when I should have been using 'React.Component'.

Gus T Butt
  • 197
  • 2
  • 4
2

I was updating and returning the same object passed to my reducer. I fixed this by making a copy of the element just before returning the state object like this.

Object.assign({}, state)
1

I was going through same issue in React-Native where API response & reject weren't updating states

apiCall().then(function(resp) { this.setState({data: resp}) // wasn't updating }

I solved the problem by changing function with the arrow function

apiCall().then((resp) => {
    this.setState({data: resp}) // rendering the view as expected
}

For me, it was a binding issue. Using arrow functions solved it because arrow function doesn't create its's own this, its always bounded to its outer context where it comes from

AndroConsis
  • 458
  • 4
  • 16
  • I have this issue and everything is bound in the constructor and all promises have arrow functions in them. The fields are rendered defaulted at empty or zero (number). When triggering a refresh they blink the values and then go to zero. But thing is, when I key in something in one fill, they all refresh the render and show their values. – TheBigCheese Nov 12 '19 at 15:49
0

After looking into many answers (most of them are correct for their scenarios) and none of them fix my problem I realized that my case is a bit different:

In my weird scenario my component was being rendered inside the state and therefore couldn't be updated. Below is a simple example:

constructor() {
    this.myMethod = this.myMethod.bind(this);
    this.changeTitle = this.changeTitle.bind(this);

    this.myMethod();
}

changeTitle() {
    this.setState({title: 'I will never get updated!!'});
}

myMethod() {
    this.setState({body: <div>{this.state.title}</div>});
}

render() {
    return <>
        {this.state.body}
        <Button onclick={() => this.changeTitle()}>Change Title!</Button>
    </>
}

After refactoring the code to not render the body from state it worked fine :)

Dharman
  • 30,962
  • 25
  • 85
  • 135
Sales Lopes
  • 131
  • 1
  • 5
0

If someone is here for similar problem, but using React Functional components rather class components AND also using react reducer, --> Move your api call outside of the reducer. Reducer should never do an api call. Refer to https://stackoverflow.com/a/39516485/12121297 for detailed response

vinodhraj
  • 177
  • 1
  • 7
0

In my case the issue was related to a child component. In a nutshell:

  • Parent component updates the state of an array of objects and forwards the updated array to a child component.
  • The child component receives the array, creates a copy of if (spread operator) and uses it to update an internal stateful component which is supposed to be rendered.
  • However, react does not re-render the child component when the update occurs in the parent component.

I solved my problem by adding a useEffect hook in the child components.

I still don’t really understand how the state update is handled in my case.

The code is available in my stackblitz repo here: https://stackblitz.com/edit/react-hel9yv?file=src%2FApp.js