Ember Route Hooks — A Complete Look

If you are like me and want to get your hands on something to hack around, I created this Ember Twiddle embedded below. Play around with it to get a better feel for Ember’s route hook order. Add your own custom logging, transitions, nested routes and other things.

The route hook lifecycle in Ember can be broken up into two distinct phases: the validation phase and the setup phase. These two phases are surrounded by two separate actions that get triggered within the context of the route: willTransition() at the beginning and didTransition() at the end after a successful transition.

Please note that below I will simply describe the most general route transition case. Additionally, I will outline, in call order, the major routing hooks which will be triggered for the route transition’s lifecycle. The major hooks being those which I believe are the most useful to understand.

Starting the Route Transition

Given that you will begin inside of a specific route within your Ember application and take one of the following actions to perform a route transition: click on a {{#link-to}} Handlebars helper or click on a UI element which performs a programmatic transitionTo() or replaceWith() call to navigate into another route.

1. willTransition

Now, Ember begins to construct the route transition. The willTransition() event gets fired with the transition argument on the currently active routes. This is probably one of the best places to prevent the transition from occurring. For example, imagine your user is currently filling out some form data but clicks on a header navigation link by accident. In this hook you could perform a quick analysis for the non-submitted data and show a modal prompt for a much better user experience.

If the transition was not terminated then we proceed to the validation phase.

Validation Phase

The purpose of this phase is to resolve all model promises for new routes. You may also use these hooks as a great opportunity to redirect the user to a different route. Keep in mind, if any of these hooks return a promise then the transition will pause until that promise either fulfills or rejects. If the project rejects, an error() event will trigger with the error and transition arguments from the current route context and will bubble up the entire router hierarchy.

2. beforeModel

The beforeModel() hook gets called with a single transition argument. This hook is best used as a place to redirect to another route before needing to execute the current route’s model() hook — if you don’t need any information that is contained within that model. Also, this is a great place to fire some asynchronous operations before the current model is resolved.

NOTE: The transition argument here and with each of the following hooks can be used to abort the current transition (e.g. an unauthenticated user accesses an authenticated route). You can also save this argument as a chunk of application state inside a service and use it again at a later point in time to retry the previously aborted transition (e.g. deep-linking a user).

3. model

The model() hook is called with the params and transition arguments and generally has one purpose to convert the URL into the model for this current route context. This usually means reaching into the Ember Data store to retrieve your model data.

// app/routes/post.js
import Ember from 'ember';

const { Route } = Ember;

export default Route.extend({
  model(params/*, transition*/) {
    return this.store.findRecord('post', params.post_id);
  }
});

NOTE: If you transition into a route via a dynamic route segment through a transition and a model context is already provided — this hook is not called. A model context does not include a primitive string or number, which does cause the model hook to be called.

4. afterModel

The afterModel() hook gets triggered next with the resolvedModel and transition arguments immediately after this route’s model has been resolved. This hook — along with the next hook — is best suited for performing any logic that must take place using information and data from this current model.

5. redirect

The redirect() hook is nearly identical to it’s predecessor afterModel() and gets called with the model and transition arguments. The important distinction here between these two hooks is simply that once you have reached the redirect() hook in the lifecycle, the current route would now be considered “active” or “fully validated”.

// app/router.js
Router.map(function() {
  this.route('posts', function() {
    this.route('post', { path: '/:post_id' });
  });
});

For example, if your current router hierarchy looks like the snippet above and we are currently in the context of the redirect() hook of the posts route. This means that if you now redirected into the child post route, the beforeModel(), model(), and afterModel() hooks for the posts route will NOT be triggered again within the new redirecting transition — as opposed to actually being triggered again if you had instead redirected from the previous afterModel hook in the posts route.

Once we have completely resolved the prior redirect() hook then we can begin to move forward and perform the setup phase.

Setup Phase

The purpose of this phase is to finally exit all of the previous routes and enter all of the new routes.

6. exit (private method)

The exit() hook is actually a private method but I still find it useful when constructing a mental model about how each of these lifecycle pieces connect together. Ember does some internal work here to immediately trigger the next deactivate() event as well as do some work to teardown views.

7. deactivate

The deactivate() event is called with no arguments and it is executed when the router completely exits a route. Note that this hook will not execute when the model for the current route changes (e.g. going from post/1 to post/2). deactivate() is best used as place do any work immediately before leaving a certain route (e.g. tracking page leave analytics).

8. enter (private method)

The enter() hook is a private method as well; however, it’s still useful to help gain a better understaning for the big picture. This hook’s only internal responsibility is to trigger the next event, activate().

9. activate

The activate() event is called with no arguments and it is executed when the router enters the new route. In the past, similar to deactivate(), I have found this to be a great place to trigger various analytics handlers.

10. setupController

The setupController() hook is called with the controller (controller for the current route) and model arguments. This sole job of this hook is to set the model property of the controller to the corresponding template property named model.

I strongly avoid using this hook whenever possible because it has personally come back to bite me with some nightmare debugging scenarios. If I want to do something like alias my model name, then I will do this inside of the actual Controller instead.

WARNING: As with all Ember hooks, if you decide to implement the setupController() hook inside your route, it will prevent the default behavior. To save yourself some headaches, remember to preserve that behavior and make sure to always call this._super(...arguments).

// app/route/application
import Ember from 'ember';

const { Route, set } = Ember;

export default Route.extend({
  setupController(controller/*, model*/) {
    // Hey Ember! Don't forget to do your stuff!
    this._super(...arguments)
    
    // OK good, Ember is done with it's internal work
    // Now I'm going to ignore Alex's advice and do some stuff here anyway :)
    set(controller, 'foo', 123);
  }
});

11. renderTemplate

The renderTemplate() hook gets called with the same controller and model arguments from the above setupController(). Internally, this hook is used to render the template for your current route. By default, it renders your route’s template, configured with the current controller for the route.

Once again, I do strongly avoid using this hook whenever possible. In the past, I have done some complex custom rending logic here for multiple templates and transitions — it can become a pain to debug and refactor when you start to really hijack the things Ember does so well under the hood. A great rule of thumb here is to just follow the “Ember Way” as much as possible. These hooks are very powerful tools and with all of that power comes some big responsibilities.

Finishing the Route Transition

Finally the route transition has completed and Ember’s internal transition promise has successfully resolved.

12. didTransition

Last but not least, the didTransition event gets fired with no arguments once the transition has successfully completed. This event is best used for some more analytics handing or setting/resetting state within the application.

Additionally, here are some great follow up resources to learn and play more with these concepts to help gain a more complete understanding: