How does Ember decide whether to update components under the hood?


#1

For some reason, our project is setting a component’s props inside didRender function, which would trigger another rerender based on the lifecycle of Ember component. But this hasn’t ended up in an infinite loop except when the previous prop and new prop are both empty arrays. May I know why is that? Could it be related to the way Ember is comparing/updating components under the hood?

someComponent.hbs

{{someComponent
    someProps=DS.attr() // THIS IS AN ARRAY
 }}

someComponent.js

didRender() {
    const newProps = ['newValue'];
    this.set('someProps', newProps)
}

So the above example works as excepted ( render -> didRender -> updateProps -> rerender -> didRender -> stop ) when both or either of the previous or current values of someProps are NOT empty. But an infinite loop occurs when BOTH of the previous and current values of someProps are empty arrays.

Any suggestion would be much appreciated! Thank you!


#2

Ember uses a caching system that will compare values and eventually settle down as those values no longer change. The guides offer a very detailed explanation of the Ember Run Loop and how it works. I don’t think it would be helpful to repeat that here. Instead I will try to highlight the problems with side effects in a didRender hook.

There are some problems above. First didRender is called every time the component renders. Since this.set() will schedule another iteration through the run loop didRender will get called again. Arrays are compared by reference and since the didRender above will set the value to a new array each time you’ve it creates an infinite loop.

  1. Render and call didRender
  2. Set property to a new array
  3. Compare the old value with the new value (will always be false as [] !== [])
  4. Schedule another iteration through the run loop to re-render changed values.
  5. Go to step 1

On a more meta level the code above asserts that by the very nature of placing this component in the DOM there will be a mutation on an ember-data model. This is pretty significant and makes the act of rendering a component a destructive thing to do. There is an implicit trust with programmers that placing a component in a template is not a destructive act and the above breaks that trust. It is better to be explicit when something will perform a destructive act. In this case the destructive act is hidden by the act of rendering which are two very separate things and no one would expect them to be coupled together like that.

Third, {{someComponent someProps=DS.attr()}} is not valid syntax; though I think I guessed at your meaning.

You need to look at the overall design of the app and understand the intent the original coders had for adding such side effects to the components and move that logic elsewhere; possibly using Service objects to mediate the data management. Unfortunately, any further advice at this time would be pure conjecture and not very helpful without having more context into the corner your code is painted into.

It would also help to consider avoiding the two-way binding and use a DDAU pattern with components.


#3

Hi Sukima,

Thank you very much for the detailed explanation! I understand the risk of using this.set() inside didRender. But what I’m trying to understand here is why exactly does Ember treat two empty arrays as different values. I know that Javascript treats two arrays/objects with the same value as different, but as you mentioned,

Ember uses a caching system that will compare values and eventually settle down as those values no longer change.

From what I understand, when Ember compare two nonEmpty arrays of two different pointers, it would stop the rerendering when the values of those two arrays are the same. But why wouldn’t this apply to two empty arrays as well?


#4

This is an observation I would never have expected. Is this true? There may be a bug somewhere. I would not rely on this as truth. It is likely some kind of unintended consequence.

In any case this situation is something to avoid all together.