Don't use afterModel for redirection

09 February 2018

An advantage about going through my book for a rewrite is that I am inclined to review things I wouldn’t when doing simple version upgrades. In the below post I want to show you one of the things I learned this way.

Redirecting during a route transition

If you want to redirect your app to another route while you’re already in the middle of a transition, there are two scenarios.

If you don’t need the resolved model

If you don’t need to resolve the model to decide whether to redirect (or where to redirect), you should put the redirection in the beforeModel hook which is the first one to run. The classic example is protecting an authenticated route:

1
2
3
4
5
6
7
8
9
// app/routes/authenticated.js
export default Route.extend({
  beforeModel() {
    if (!this.get('isAuthenticated')) {
      return this.transitionTo('login');
    }
  },
    (...)
});

If you need the resolved model

If you need to know what the model is to be able to decide on the redirection, you need a hook that runs after the model hook. I’ve so far used the afterModel hook for this purpose that gets passed the resolved model.

The official guides use the “redirect to the only existing blog post” example. I’m going to use another one - not because there’s anything wrong with this one, but because that’s the one in my book.

When clicking a band link, we want to see the band’s details page if the band has a description - otherwise we want to see the list of the band’s songs.

Here’s what the template with the band link looks like:

1
2
3
4
5
6
7
<!-- app/templates/bands.hbs -->
{{#link-to "bands.band" band.id}}
  {{band.name}}
  <span class="pointer">
    {{fa-icon "angle-right"}}
  </span>
{{/link-to}}

Let’s now see the corresponding route hook that implement the redirection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/routes/bands/band.js
export default Route.extend({
  model(params) {
    return this.store.findRecord('band', params.id);
  },

  afterModel(band) {
    if (band.get('description')) {
      this.transitionTo('bands.band.details');
    } else {
      this.transitionTo('bands.band.songs');
    }
  }
});

This works fine but let’s see the route transitions in detail.

Logging out route transitions

Route transitions are logged verbosely if you enable the following lines in your configuration:

1
2
3
4
5
6
7
// config/environment.js
  if (environment === 'development') {
    (...)
    ENV.APP.LOG_TRANSITIONS = true;
    ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
    (...)
  }

Also, you shouldn’t forget to show debug messages in your browser console:

Enable console.debug

afterModel is wasteful, …

Ok, let’s see what route transitions and hooks are executed when we trigger a redirection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Attempting transition to bands.band
Preparing to transition from 'bands.index' to 'bands.band.index'
Transition #3: bands.band: calling beforeModel hook
Transition #3: bands.band: calling deserialize hook
Transition #3: bands.band: calling afterModel hook
Attempting transition to bands.band.details
Transition #3: bands.band.index: transition was aborted
Transition #4: bands.band: calling beforeModel hook
Transition #4: bands.band: calling deserialize hook
Transition #3: detected abort.
Transition #4: bands.band: calling afterModel hook
Attempting transition to bands.band.details
Transition #4: bands.band.details: calling beforeModel hook
Transition #4: bands.band.details: calling deserialize hook
Transition #4: bands.band.details: calling afterModel hook
Transition #4: Resolved all models on destination route; finalizing transition.
Transition #4: TRANSITION COMPLETE.
XHR finished loading: GET "http://rarwe.local/bands/pearl-jam".
XHR finished loading: GET "http://rarwe.local/bands/pearl-jam".

(deserialize is just an alias for model, so read it as such.)

You can see that the first transition, to bands.band.index, is aborted because of the this.transitionTo in the afterModel of the bands.band route and then a new one is attempted to bands.band.details. However, all three model hooks are run again for the bands.band route (see lines 8-11).

Also note that there are two ajax requests going out. The store.findRecord('band', ...) call is responsible for this, as it will fetch the band from the backend (in the background if it has found it in the store so as not to block page rendering).

Since the model hook of the bands.band route was fired twice, the ajax request is fired twice, too.

… so use the redirect hook

There is another, possibly less known route hook called redirect, which is more suitable for redirection. I know, who would’ve thought, right?

I thought it was basically an alias for afterModel, but there is a very important difference in their behavior. The source code shines a light on this distinction (see here):

redirect and afterModel behave very similarly and are called almost at the same time, but they have an important distinction in the case that, from one of these hooks, a redirect into a child route of this route occurs: redirects from afterModel essentially invalidate the current attempt to enter this route, and will result in this route’s beforeModel, model, and afterModel hooks being fired again within the new, redirecting transition. Redirects that occur within the redirect hook, on the other hand, will not cause these hooks to be fired again the second time around; in other words, by the time the redirect hook has been called, both the resolved model and attempted entry into this route are considered to be fully validated.

Let’s thus change our afterModel hook to redirect:

1
2
3
4
5
6
7
// app/routes/bands/band.js
export default Route.extend({
-  afterModel(band) {
+  redirect(band) {
    (...)
  }
});

Let’s see the route transition details again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Attempting transition to bands.band
Preparing to transition from 'bands.index' to 'bands.band.index'
Transition #1: bands.band: calling beforeModel hook
Transition #1: bands.band: calling deserialize hook
Transition #1: bands.band: calling afterModel hook
Attempting transition to bands.band.details
Transition #1: bands.band.index: transition was aborted
Transition #1: detected abort.
Transition #2: bands.band.details: calling beforeModel hook
Transition #2: bands.band.details: calling deserialize hook
Transition #2: bands.band.details: calling afterModel hook
Transition #2: Resolved all models on destination route; finalizing transition.
Transitioned into 'bands.band.details'
Transition #2: TRANSITION COMPLETE.
XHR finished loading: GET "http://rarwe.local/bands/pearl-jam".

The model hooks for bands.band are not fired a second time, and so there is only one ajax request hitting the backend and less time spent in model hooks (and consequently the page can be rendered faster).

So, in summary, don’t use the afterModel for redirection. Use redirect.