Neat Facts About JavaScript & Ember

I wanted to share a couple of neat facts or potential gotchas (depending on how you look at it) that have come up while doing JavaScript programming with Ember.js recently.

The first one has to do with the fact that JavaScript passes primitives, such as booleans, strings, and numbers, by value, and non-primitives, such as objects and arrays, by reference. If you create a non-primitive property on a JavaScript object’s prototype, then all instances of that object will share the same property value by default. This is not specific to Ember but comes up often when setting an array or object property on an Ember Object. If you add an array property to the prototype of an object, then all instances of that object will share the same array, which is most likely not what you want. For example:

// my-component.js
import Ember from 'ember';
export default Ember.Component.extend({
  // All instances will share the same array!
myArray: [],
  init() {
this._super(...arguments);
this.get('myArray').push('hello')
}
});
// my-component.hbs
{{#each myArray as |str|}}
{{str}}
{{/each}}
// application.hbs
{{my-component}} {{! output: hello}}
{{my-component}} {{! output: hello hello}}
{{my-component}} {{! output: hello hello hello}}

By the time of the 3rd component invocation, 3 strings with "hello" have been added to the same array! This is because the property myArray is assigned a value [] exactly once when the file is first read by the browser. Here are two ways to solve this:

  1. Lazily return the array in a computed property:
export default Ember.Component.extend({
myArray: computed(function() {
return [];
}
});

This solution works because the line returning the array is not executed until the component instance is initialized and the property is fetched. Therefore, each component instance will have a unique array in the myArray property.

2. Set the property in the init().

export default Ember.Component.extend({
myArray: null,
  init() {
this._super(...arguments);
this.set('myArray', []);
this.get('myArray').push('hello');
}
});

Again, that line that creates the array and sets it to the myArray property is executed for each component instance. To see how this works in detail, try it out in this twiddle.

Twiddle

As Cory Forsyth reminded me, the fact that arrays and objects are passed by reference also means that const in ES6 does not behave exactly as you might think. It creates a mutable binding to a reference rather than an immutable value. For example:

const str = "hello";
str = "world" // Will throw an error
const a = [1,2,3];
a[0] = 5; // Completely valid, a is now [5, 2, 3]
a.push(4); // Completely valid, a is now [5, 2, 3, 4]
a = [5, 6, 7]; // will throw an error

You can read more about const in the article “What does const stand for in ES6?

Keep Your Route’s model Property in Sync with your Controller

I thought of this during Balint Erid’s fantastic talk about Ember Data at Ember Conf 2017. There are legitimate circumstances where you may want to immediately return one thing from a Route’s Model hook, for example cached data, and then asynchronously fetch new data and replace the model on the controller. However, it’s important to know that the value of the Route’s modelFor() function will retain the old value.

A developer familiar with Ember is likely to assume that for a route called post, the model property on the post controller, and the result of calling this.modelFor('post') from any other route, will always be the same. The async data fetching technique described above could break this assumption if not implemented carefully. Here’s an example which you can also try out using this Twiddle:

import Ember from 'ember';
export default Ember.Route.extend({
model() {
Ember.run.schedule('afterRender', () => {
this.controller.set('model', 'world');
      // This will always return 'hello' 
// even though the controller's model just changed.
const routeModel = this.modelFor('application');
this.controller.set('routeModel', routeModel);
});
    return 'hello';
}
});

The template will look like this:

{{model}} {{! "world" the most up-to-date-value}}
{{routeModel}} {{! "hello" the original value}}

This example will fail even if you wrap the call to this.modelFor('application') in a separate run loop. Excercize for the reader: Try this out on your own in the Twiddle.

A better solution might be to wrap the model in an object or array that can remain consistent:

export default Ember.Route.extend({
model() {
Ember.run.schedule('afterRender', () => {
this.controller.set('model.name', 'world');

const routeModel = this.modelFor('application');
this.controller.set('routeModel', routeModel);
});
    return {
name: 'hello'
};
}
});
// template
{{model.name}} {{! world}}
{{routeModel.name}} {{! world, same as model}}

Another option is to recall that if you call Route.transitionTo and pass it an object as the 2nd argument (instead of a string or number), the router will set that model on the route and controller without calling the model, beforeModel, and afterModel hooks. This only works if your route has at least one dynamic segment, that is, at least 1 dynamic parameter in the URL such as “my-route/:id”. This also requires that your model is an object and not a primitive type like a string or a number. Here is what that looks like:

export default Ember.Route.extend({
model() {
Ember.run.schedule('afterRender', () => {
// start transition with new name object
this.transitionTo('application', { name: 'world' });
      // schedule another run loop after the transition is complete
Ember.run.schedule('afterRender', () => {
const routeModel = this.modelFor('application');
this.controller.set('routeModel', routeModel);
});
});
    return { name: 'hello' };
}
});

All 3 examples are in the Twiddle.

That’s it for today. I hope you learned something useful about Ember. If you have any questions or comments please let me know. If you need help building your Ember.js app, please reach out to 201 Created.