The 8 Most Common Mistakes That Ember.js Developers Make
Ember.js is a comprehensive framework for building complex client-side applications. But, as with any advanced framework, there are still pitfalls Ember developers may fall into. With the following post, I hope to provide a map to evade these. Let’s jump right in!!
Ember.js is a comprehensive framework for building complex client-side applications. But, as with any advanced framework, there are still pitfalls Ember developers may fall into. With the following post, I hope to provide a map to evade these. Let’s jump right in!!
Balint has been practicing TDD since before it became popular. He was a classic PHP coder, and has since moved on to Java, Python, and Ruby.
Expertise
PREVIOUSLY AT
Ember.js is a comprehensive framework for building complex client-side applications. One of its tenets is “convention over configuration,” and the conviction that there is a very large part of development common to most web applications, and thus a single best way to solve most of these everyday challenges. However, finding the right abstraction, and covering all the cases, takes time and input from the whole community. As the reasoning goes, it is better to take the time to get the solution for the core problem right, and then bake it into the framework, instead of throwing up our hands and letting everybody fend for themselves when they need to find a solution.
Ember.js is constantly evolving to make development even easier. But, as with any advanced framework, there are still pitfalls Ember developers may fall into. With the following post, I hope to provide a map to evade these. Let’s jump right in!
Common Mistake No. 1: Expecting the Model Hook to Fire When All Context Objects Are Passed In
Let’s assume we have the following routes in our application:
Router.map(function() {
this.route('band', { path: 'bands/:id' }, function() {
this.route('songs');
});
});
The band
route has a dynamic segment, id
. When the app is loaded with a URL like /bands/24
, 24
is passed to the model
hook of the corresponding route, band
. The model hook has the role of deserializing the segment to create an object (or an array of objects) that can then be used in the template:
// app/routes/band.js
export default Ember.Route.extend({
model: function(params) {
return this.store.find('band', params.id); // params.id is '24'
}
});
So far so good. However, there are other ways to enter routes than loading up the application from the browser’s navbar. One of them is using the link-to
helper from templates. The following snippet goes through a list of bands and creates a link to their respective band
routes:
{{#each bands as |band|}}
{{link-to band.name "band" band}}
{{/each}}
The last argument for link-to, band
, is an object that fills in the dynamic segment for the route, and thus its id
becomes the id segment for the route. The trap that many people fall into is that the model hook is not called in that case, since the model is already known and has been passed in. It makes sense and it might save a request to the server but it is, admittedly, not intuitive. An ingenious way around that is to pass in, not the object itself, but its id:
{{#each bands as |band|}}
{{link-to band.name "band" band.id}}
{{/each}}
Ember’s Mitigation Plan
Routable components will be coming to Ember shortly, probably in version 2.1 or 2.2. When they land, the model hook will always be called, no matter how one transitions to a route with a dynamic segment. Read the corresponding RFC here.
Common Mistake No. 2: Forgetting That Route-driven Controllers Are Singletons
Routes in Ember.js set up properties on controllers that serve as the context for the corresponding template. These controllers are singletons and consequently any state defined on them persists even when the controller is no longer active.
This is something that is very easy to overlook and I stumbled into this, too. In my case, I had a music catalogue app with bands and songs. The songCreationStarted
flag on the songs
controller indicated that the user has begun creating a song for a particular band. The problem was that if the user then switched to another band, the value of songCreationStarted
persisted, and it seemed like the half-finished song was for the other band, which was confusing.
The solution is to manually reset the controller properties we don’t want to linger. One possible place to do this is the setupController
hook of the corresponding route, which gets called on all transitions after the afterModel
hook (which, as its name suggests, comes after the model
hook):
// app/routes/band.js
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
controller.set('songCreationStarted', false);
}
});
Ember’s Mitigation Plan
Again, the dawn of routeable components will solve this problem, bringing an end to controllers altogether. One of the advantages of routeable components is that they have a more consistent lifecycle and always get torn down when transitioning away from their routes. When they arrive, the above problem will vanish.
Common Mistake No. 3: Not Calling the Default Implementation in setupController
Routes in Ember have a handful of lifecycle hooks to define application-specific behavior. We already saw model
which is used to fetch data for the corresponding template and setupController
, for setting up the controller, the template’s context.
This latter, setupController
, has a sensible default, which is assigning the model, from the model
hook as the model
property of the controller:
// ember-routing/lib/system/route.js
setupController(controller, context, transition) {
if (controller && (context !== undefined)) {
set(controller, 'model', context);
}
}
(context
is the name used by the ember-routing
package for what I call model
above)
The setupController
hook can be overridden for several purposes, like resetting the controller’s state (as in Common Mistake No. 2 above). However, if one forgets to call the parent implementation that I copied above in Ember.Route, one can be in for a long head-scratching session, as the controller will not have its model
property set. So always call this._super(controller, model)
:
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
// put the custom setup here
}
});
Ember’s Mitigation Plan
As stated before, controllers, and with them, the setupController
hook, are going away soon, so this pitfall will be a threat no longer. However, there is a greater lesson to be learned here, which is to be mindful of implementations in ancestors. The init
function, defined in Ember.Object
, the mother of all objects in Ember, is another example you have to watch out for.
Common Mistake No. 4: Using this.modelFor
with Non-parent Routes
The Ember router resolves the model for each route segment as it processes the URL. Let’s assume we have the following routes in our application:
Router.map({
this.route('bands', function() {
this.route('band', { path: ':id' }, function() {
this.route('songs');
});
});
});
Given a URL of /bands/24/songs
, the model
hook of bands
, bands.band
and then bands.band.songs
are called, in this order. The route API has a particularly handy method, modelFor
, that can be used in child routes to fetch the model from one of the parent routes, as that model has surely been resolved by that point.
For example, the following code is a valid way to fetch the band object in the bands.band
route:
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
var bands = this.modelFor('bands');
return bands.filterBy('id', params.id);
}
});
A common mistake, however, is to use a route name in modelFor that is not a parent of the route. If the routes from the above example were slightly altered:
Router.map({
this.route('bands');
this.route('band', { path: 'bands/:id' }, function() {
this.route('songs');
});
});
Our method to fetch the band designated in the URL would break, as the bands
route is no longer a parent and thus its model has not been resolved.
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
var bands = this.modelFor('bands'); // `bands` is undefined
return bands.filterBy('id', params.id); // => error!
}
});
The solution is to use modelFor
only for parent routes, and use other means to retrieve the necessary data when modelFor
cannot be used, such as fetching
from the store.
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
return this.store.find('band', params.id);
}
});
Common Mistake No. 5: Mistaking the Context a Component Action Is Fired On
Nested components have always been one of the most difficult parts of Ember to reason about. With the introduction of block parameters in Ember 1.10, a lot of this complexity has been relieved, but in many situations, it’s still tricky to see at a glance what component an action, fired from a child component, will be triggered on.
Let’s assume we have a band-list
component that has band-list-items
in it, and we can mark each band as a favorite in the list.
// app/templates/components/band-list.hbs
{{#each bands as |band|}}
{{band-list-item band=band faveAction="setAsFavorite"}}
{{/each}}
The action name that should be invoked when the user clicks on the button is passed into the band-list-item
component, and becomes the value of its faveAction
property.
Let’s now see the template and component definition of band-list-item
:
// app/templates/components/band-list-item.hbs
<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "faveBand"}}>Fave this</button>
// app/components/band-list-item.js
export default Ember.Component.extend({
band: null,
faveAction: '',
actions: {
faveBand: {
this.sendAction('faveAction', this.get('band'));
}
}
});
When the user clicks the “Fave this” button, the faveBand
action gets triggered, which fires the component’s faveAction
that was passed in (setAsFavorite
, in the above case), on its parent component, band-list
.
That trips up a lot of people since they expect the action to be fired the same way that actions from route-driven templates are, on the controller (and then bubbling up on the active routes). What makes this worse is that no error message is logged; the parent component just swallows the error.
The general rule is that actions are fired on the current context. In the case of non-component templates, that context is the current controller, while in the case of component templates, it is the parent component (if there is one), or again the current controller if the component is not nested.
So in the above case, the band-list
component would have to re-fire the action received from band-list-item
in order to bubble it up to the controller or route.
// app/components/band-list.js
export default Ember.Component.extend({
bands: [],
favoriteAction: 'setFavoriteBand',
actions: {
setAsFavorite: function(band) {
this.sendAction('favoriteAction', band);
}
}
});
If the band-list
was defined in the bands
template, then the setFavoriteBand
action would have to be handled in the bands
controller or the bands
route (or one of its parent routes).
Ember’s Mitigation Plan
You can imagine that this gets more complex if there are more levels of nesting (for example, by having a fav-button
component inside band-list-item
). You have to drill a hole through several layers from the inside to get your message out, defining meaningful names at each level (setAsFavorite
, favoriteAction
, faveAction
, etc.)
This is made simpler by the “Improved Actions RFC”, which is already available on the master branch, and will probably be included in 1.13.
The above example would then be simplified to:
// app/templates/components/band-list.hbs
{{#each bands as |band|}}
{{band-list-item band=band setFavBand=(action "setFavoriteBand")}}
{{/each}}
// app/templates/components/band-list-item.hbs
<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "setFavBand" band}}>Fave this</button>
Common Mistake No. 6: Using Array Properties As Dependent Keys
Ember’s computed properties depend on other properties, and this dependency needs to be explicitly defined by the developer. Say we have an isAdmin
property that should be true if and only if one of the roles is admin
. This is how one might write it:
isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles')
With the above definition, the value of isAdmin
only gets invalidated if the roles
array object itself changes, but not if items are added or removed to the existing array. There is a special syntax to define that additions and removals should also trigger a recomputation:
isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles.[]')
Common Mistake No. 7: Not Using Observer-friendly Methods
Let’s extend the (now fixed) example from Common Mistake No. 6, and create a User class in our application.
var User = Ember.Object.extend({
initRoles: function() {
var roles = this.get('roles');
if (!roles) {
this.set('roles', []);
}
}.on('init'),
isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles.[]')
});
When we add the admin
role to such a User
, we’re in for a surprise:
var user = User.create();
user.get('isAdmin'); // => false
user.get('roles').push('admin');
user.get('isAdmin'); // => false ?
The problem is that observers will not fire (and thus computed properties will not get updated) if the stock Javascript methods are used. This might change if the global adoption of Object.observe
in browsers improves, but until then, we have to use the set of methods that Ember provides. In the current case, pushObject
is the observer-friendly equivalent of push
:
user.get('roles').pushObject('admin');
user.get('isAdmin'); // => true, finally!
Common Mistake No. 8: Mutating Passed in Properties in Components
Imagine we have a star-rating
component which displays an item’s rating and allows the setting of the item’s rating. The rating can be for a song, a book or a soccer player’s dribble skill.
You would use it like this in your template:
{{#each songs as |song|}}
{{star-rating item=song rating=song.rating}}
{{/each}}
Let’s further assume that the component displays stars, one full star for each point, and empty stars after that, up until a maximum rating. When a star is clicked, a set
action is fired on the controller, and it should be interpreted as the user wanting to update the rating. We could write the following code to achieve this:
// app/components/star-rating.js
export default Ember.Component.extend({
item: null,
rating: 0,
(...)
actions: {
set: function(newRating) {
var item = this.get('item');
item.set('rating', newRating);
return item.save();
}
}
});
That would get the job done, but there are a couple of problems with it. First, it assumes that the passed in item has a rating
property, and so we can’t use this component to manage Leo Messi’s dribble skill (where this property might be called score
).
Second, it mutates the item’s rating in the component. This leads to scenarios where it is hard to see why a certain property changes. Imagine we have another component in the same template where that rating is also used, for example, for calculating the average score for the soccer player.
The slogan for mitigating the complexity of this scenario is “Data down, actions up” (DDAU). Data should be passed down (from route to controller to components), while components should use actions to notify their context about changes in these data. So how should DDAU be applied here?
Let’s add an action name that should be sent for updating the rating:
{{#each songs as |song|}}
{{star-rating item=song rating=song.rating setAction="updateRating"}}
{{/each}}
And then use that name for sending the action up:
// app/components/star-rating.js
export default Ember.Component.extend({
item: null,
rating: 0,
(...)
actions: {
set: function(newRating) {
var item = this.get('item');
this.sendAction('setAction', {
item: this.get('item'),
rating: newRating
});
}
}
});
Finally, the action is handled upstream, by the controller or the route, and this is where the item’s rating gets updated:
// app/routes/player.js
export default Ember.Route.extend({
actions: {
updateRating: function(params) {
var skill = params.item,
rating = params.rating;
skill.set('score', rating);
return skill.save();
}
}
});
When this happens, this change is propagated downwards through the binding passed to the star-rating
component, and the number of full stars displayed changes as a result.
This way, mutation does not happen in the components, and since the only app specific part is the handling of the action in the route, the component’s reusability does not suffer.
We could just as well use the same component for soccer skills:
{{#each player.skills as |skill|}}
{{star-rating item=skill rating=skill.score setAction="updateSkill"}}
{{/each}}
Final Words
It is important to note that some (most?) of the mistakes I have seen people commit (or committed myself), including the ones I have written about here, are going to disappear or be greatly mitigated early on in the 2.x series of Ember.js.
What remains is addressed by my suggestions above, so once you’re developing in Ember 2.x, you’ll have no excuse to make any more errors! If you’d like this article as a pdf, head over to my blog and click the link at the bottom of the post.
About Me
I came to the front-end world with Ember.js two years ago, and I am here to stay. I became so enthusiastic about Ember that I started blogging intensely both in guest posts and on my own blog, as well as presenting at conferences. I even wrote a book, Rock and Roll with Ember.js, for anyone who wants to learn Ember. You can download a sample chapter here.
Further Reading on the Toptal Blog:
Balint Erdi
Budapest, Hungary
Member since February 26, 2014
About the author
Balint has been practicing TDD since before it became popular. He was a classic PHP coder, and has since moved on to Java, Python, and Ruby.
Expertise
PREVIOUSLY AT