Web Front-end11 minute read

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!!


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

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!!


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Balint Erdi
Verified Expert in Engineering

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.

Read More

PREVIOUSLY AT

Ericsson
Share

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.js

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:

Hire a Toptal expert on this topic.
Hire Now
Balint Erdi

Balint Erdi

Verified Expert in Engineering

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.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Ericsson

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.