Readers' Questions - "Is it bad to load data in components?"


#1

Hello once again to Readers’ Questions, presented by the Ember.js Times. Today I’m answering this question by jj:

Is it still considered an anti-pattern to handle lots of data loading in the scope of Components?

In general, it’s not an anti-pattern to load data in a component. But there are two common ways people go wrong by loading data in components:

  1. They load data in a component when the router could have easily done it for them (thus spending effort and increasing complexity for no benefit).

  2. They load data in a component without thinking through all extra work the router is no longer doing for them.

When to load data in the router

The argument for using the router is:

  • respecting the URL results in better user experience

  • proper use of loading and error states result in better user experience

  • dealing with concurrency is a big source of bugs

  • the router is designed to solve those problems for you in the majority of common situations

Now, you can achieve all those goals without the router, but it’s much more work. So bias yourself toward looking for ways to leverage what the router can do before you reach for hand-rolled data loading in components.

Here are some examples of cases where you should still use the router, even thought they may seem at first to be more complex than the canonical, simple route.

Multiple Models

What if you want to load many different models, not just one? No problem.

Rendering part of the template instantly while another part is still loading

What if you want to render part of your template instantly, while showing a loading spinner for the rest? No problem: the router’s loading states apply recursively at each level in the route hierarchy. You can always start with a single route:

this.route('calendar', { path: '/calendar/:month' });

And refactor a new child route out of it:

this.route('calendar', { path: '/calendar/:month' }, function() {
  this.route('appointments', { path: '/ });
});

Now when you enter the /calendar/april URL, both the parent and child routes will immediately become active. You can choose to let the calendar.hbs template render instantly while showing calendar/loading.hbs inside it’s {{outlet}} until calendar/appointments.hbs is ready to show.

Keep in mind that child routes have full access to their ancestor’s route params and models, so it’s straightforward to move the data loading down a level.

Depending on data in services

What if you need some input from a Service in order to fetch the right data. No problem, Routes can inject Services.

Depending on ephemeral state that’s not in the URL

What if the data you need to load depends on some ephemeral state that’s not part of the URL? For example, maybe the user can toggle a detail pane open and closed.

This can be a hint from Ember that you probably should reflect that state in the URL. It will make your app more robust, easier to develop (because everything will stay where you left it across code-change-triggered reloads), and it will let you lean on the router to do the proper data loading.

When you can’t use (just) the router

There are some cases where the router can’t do it all for you.

Multiple Independent route hierarchies

If you have two panes, and each one can navigate entirely independently, the router isn’t going to be able to do your data loading for you. You’re better off putting the router in charge of one of them, and doing more manual data loading for the other.

Another example where this comes up is a modal that can appear during many different routes, in which the modal itself is rich enough that it really wants to have its own routes inside.

Parallel data loading in deep hierarchies

If you have a deep hierarchy of routes, and each layer is loading data, and the loads don’t depend on each other, the router will load them in series even though in principle they could have been loaded faster, in parallel. This is a consequence of the fact that modelFor is synchronous, so child routes always wait for their parent’s models to be available (changing modelFor to return a promise instead so we can avoid this limitation is on my personal wishlist for future Route API changes).

If you run into this situation, you can still initiate data loading in the router, but you won’t want to wait around for it to finish at each layer. One way to do this is to wrap your pending promise inside another object and return that from your model hook. Then it will be up to your components to await the promises and deal with loading, rejection, etc.

Highly-interactive loading

Consider a Google-like search interface with realtime suggestions. While it’s possible to do even that kind of UI with in-router data loading (via features like queryParams with refreshModel enabled), you may want finer-grained control over things like debouncing, cancelation, when to show stale data while waiting for newer data, when to show spinners, etc. This is a valid use case for loading data in components.

How not to mess up when you’re rolling your own

If you’re loading data in a component, it’s very easy to miss all the ways concurrency can mess you up. Thankfully, we have ember-concurrency for that. Definitely use ember-concurrency, and learn about restartable (which is usually what you want for data loading tasks).

Another way it’s easy to mess up is forgetting to keep your application state bound properly to the URL, thus breaking the back button and making refreshes lose state. Ideally you should still get parameters from the router, even if you aren’t using the router to load the data. This can involve either queryParams or having model hooks that just send params straight through:

model(params){ return params; }

Finally, don’t forget to handle things like rejected promises and loading states. Instead of having separate templates rendered automatically for you by the router, you’ll need conditionals in your own template. ember-concurrency’s derived state features like isRunning and isError can help with this. It’s much more reliable to use derived state than to try to track those things yourself with Ember.set.

This answer will be published in the upcoming issue of The Ember.js Times. You’re getting it early because our Discourse forum is awesome and I’m happy to see more content and activity happening here.


How do a test promise isFulfilled in integration tests?
#2

Good question, great comprehensive answer. Thank you!


#3

Thank you for this detailed write-up @ef4!


#4

Thanks for taking the time to write this up. Us newbies need all the help we can get - especially when it comes to the fundamentals.


#5

Agreed, I’m a “beginner” for all intents and purposes


#6

RSVP functions like all and hash can be used in components just as well. Same goes for injecting services.

The main disadvantage of loading data in components is that you can easily loose track of where all data is being loaded and the risk of making the same request multiple times.


#7

Not only that, but it can also make your component less generic. Components like ember-power-select is a great example where we should have a well-defined data API, but do not specialize.

Now if we have a container component just to fetch/update data, that’s fine!


#8

I think the presentational and container is a nice pattern to have. Definitely worth a read, although the article is based on react it can easily be applied to any component framework.