Andrey Mikhaylov

Andrey Mikhaylov @lolmaus

frontend developer, EmberJS enthusiast
← To blog index

Do return an RSVP.hash() from your routes' model hooks!

8th December 2016

It's a relatively widespread opinion that returning a hash from the model hook is a bad practice. I believe it's not! I always return a hash, following a witty pattern, and find it very beneficial.

I've never thought it's a bad thing in the first place, so let's see what a smarter Ember dev thinks.

Sam Selikoff, the heroic author of Mirage, in his blog post supports the idea of never returning a hash from the model hook.

Though I definitely follow Sam's example on Ember patterns, I dare to disagree with this particular case.

Here are some of his points summarized by me. Make sure to read the original post!

  • The hook's name, model, implies that a single entity should be returned from it.
  • A necessity to return more than one entity is an indication of bad ERM design.
  • If you need to return several different models, you should refactor by introducing a junction model with relationships that represent a combination of the models you need, and return that junction model from the route.
  • If the models are so independent that it's inappropriate to unite them under a single entity, you shouldn't load all of them in the route. Instead, load the most important one from the route and load the rest from the controller/components after the initial render.
  • In Rails, controllers should instantiate only one object.

Let's see.

The hook's name, model, implies that a single entity should be returned from the hook.

The name of the hook isn't really a decisive factor to me. First of all, in web dev, the word model is used in a very wide meaning. In the context of MVC's model layer such as Ember Data, "model" typically means a class representing a resource and is used to instantiate individual records of that resource. But outside the model layer context, a "model" is simply something that represents your data, and it can be anything — from a string to a complex, arbitrarily defined JSON-like structure.

Also, Sam is not against returning an array from the model hook, even though its name isn't models.

If you need to return several models, you should refactor by introducing a junction model.

If you can't unite unrelated models under a single entity, you shouldn't load all of them in the route.

I find the recommendation to refactor the ERM in such a way, that every route can be represented with a single entity, to be idealistic and naive: it is simply not always possible!

Quite often you do have routes that display several unrelated, yet equally important entities. Displaying the route without some of them makes no business-logical sense, yet you can't reasonably unite them under a single junction entity.

Even if introducing a junction entity makes sense, updating the backend can be too hard or even impossible, for example, if you don't have access to and/or authority over the backend codebase.

You could introduce a frontend-only junction model. In certain complicated cases, this is the optimal solution, and I did follow this path when it was appropriate (example). But the cost is high: your frontend and backend ERMs diverge. I believe, doing that simply to avoid returning a hash is absolutely unreasonable.

Even if you can synchronize the ERM refactor of the frontend and the backend, it's still a terribly huge stretch for avoiding one trivial pattern.

In Rails, controllers should instantiate only one object.

Rails isn't really something one should Compare Ember with. Though both are considered MVC frameworks, their architecture is very different. Rails doesn't have a "Route" class at all, and for a specific REST call the router always calls exactly one controller, even if the URL represents a nested resource. In Ember, Routes are entities that a responsible for data loading and are called in a chain.

But most importantly, Sam doesn't point out any practical disadvantage of returning a hash. That's because there are none! But there are benefits.

Let me explain how I do it and then we'll see what the advantages are.

Every route's model hook should return an RSVP.hash(). Even if it loads only one entity, put it into a property on the hash.

The trick is that every model hash should extend its parent hash. Except for topmost routes which have no parent model, of course.

Here's how my typical model hooks look like:

// posts route
model () {
  const store = this.get('store')

  return RSVP.hash({
    posts: store.findAll('post'),
  })
}
// posts.post route
{
  model ({postId}) {
    const store = this.get('store')
    const model = this.modelFor('posts')

    return RSVP.hash({
      ...model,
      currentPost: store.peekRecord('post', postId),
    })
  }
}

The ... is a spread operator, short for Ember.merge and Object.assign.

The posts.post route will end up with a model like this:

{
  posts: [post1, post2, post3],
  currentPost: post2
}

Now let's how it makes your codebase awesome!

One of Sam's arguments I didn't mention earlier is that accessing model.posts in a template is worse than just model. I disagree.

When I see model used in a template, it's always puzzling and frustrating. Seeing model.posts, on the other hand, is self-explanatory.

With this approach, the model property in every controller always contains all data that has been loaded in all of the parent routes in the hierarchy.

You can access that data in any template directly, without having to manually pass this data through by using hairy patterns like modelFor in setupController, unnecessary services or junction models.

Say, on an individual post route I would like to show links to previous and next posts. Here's what I'm not gonna do in order to achieve that:

  • I don't have to introduce previousPost and nextPost relationships on the post model.
  • I don't need to introduce a junction model that represents a post with its adjacent posts.
  • I don't have to run store.peekRecord on the controller/component level.
  • I don't need to use setupController. I find setupController to be a bad practice that abandons declarative computed properties in favor of ugly imperative code that increases tight coupling. There's literally only one place where you need setupController: to pass an error into the error substate.
  • I don't have to introduce a service that retrieves adjacent posts for a given post.

All I need to do is to access model.posts! I don't even care in which of the parent routes it was loaded.

Note that I'm not encouraging you to preload all posts in the posts route. In the simplest case, it's more efficient to load all posts in posts.index. This way, the user won't have to download all posts if they only came to see a specific one at posts.posts.

But it's a very common situation that you need to load all records in records in the parent due to other reasons. For example, you want to display a tag cloud, but your backend doesn't provide a dedicated tags API: tags are simply an attribute on the model. Or you want to display a list of recent posts in the sidebar but your API can't filter by date and limit amount, so you have to do it on the client. Or there's simply not that much records of a certain type, so it's both fast and convenient to preload all of them.

In such cases, you have your records preloaded anyway. So why not reuse them effectively?

So I want to display links to previous and next posts.

Knowing that all posts are available as model.posts in almost any controller, I can just toss a few computed properties where I need them:

import {sort}              from 'ember-computed'
import sum                 from 'ember-cpm/macros/sum'
import indexOf             from 'make/your/own/macro'
import getFromArrayByIndex from 'it/is/easy/and/fun'

{
  sortOrder:         'createdAd', // assuming it's adjustable by user
  sortedPosts:       sort('model.posts', 'sortOrder'),
  currentPostIndex:  indexOf('sortedPosts', 'model.currentPost'),
  nextPostIndex:     sum('currentPostIndex', 1),
  previousPostIndex: sum('currentPostIndex', -1),
  nextPost:          getFromArrayByIndex('sortedPosts', 'nextPostIndex'),
  previousPost:      getFromArrayByIndex('sortedPosts', 'previousPostIndex'),
}
{{#if nextPost}}
  {{link-to (concat '← ' nextPost.title)     'posts.post' nextPost.id}}
{{/if}}

{{#if previousPost}}
  {{link-to (concat previousPost.title ' →') 'posts.post' previousPost.id}}
{{/if}}

This code is declarative, as bug-proof as it can be and easy to understand from a single glance.

It's also performant: CP values are cached, and once you leave and revisit the route, the controller/component won't have to recalculate those values. But they'll recalculate automatically if the array of posts changes.

If you need to preload some related records, you can do it in a very readable way.

In this example, we preload authors of the post and of all its comments:

{
  model ({postId}) {
    const store = this.get('store')
    const model = this.modelFor('posts')

    return RSVP
      .hash({
        ...model,
        currentPost: store.peekRecord('post', postId),
      })

      // When we have the post, we're able to fetch its author and comments
      .then(model => RSVP.hash({
        ...model,
        author:   model.currentPost.get('author'),
        comments: model.currentPost.get('comments'),
      }))

      // And finally we're able to fetch comment authors:
      .then(model => RSVP.hash({
        ...model,
        commentAuthors: store.query('user', {
          'filter[ids]': this._getCommentAuthorIds(model.comments)
        })
      }))
  },

  _getCommentAuthorIds (comments) {
    return comments
      .map(comment => comment.belongsTo('author').id())
      .join(',')
  },
}

One of Sam's concerns is that this approach prevents devs from traversing the model graph.

I fully agree that in most cases you should traverse the model graph via a chain like model.currentPost.comments[n].author rather than filter current author's comment from model.commentAuthors.

But the matter is that though you do have model.commentAuthors available, you still can access comment authors via the chain. model.commentAuthors was merely a self-explanatory way to preload data, it does not prevent you from using the other way.

One problem with this approach is that JSHint freaks out at the ... spread operator.

It's not a problem of the approach itself but rather a matter of relying on outdated tooling.

ember install ember-eslint resolves this problem for good. You shouldn't avoid the powerful spread operator only because JSHint sucks at ES2015.

Please share your opinion on this approach in the comments section below.

Does this approach make your dev life a tad more enjoyable?

What disadvantages does it have? Can they be mitigated or is it wrong in the first place?