Route handling multiple complex components

In apps you often have to handle complex routes that interact with multiple endpoints, and I suspect the way I do it is not optimal.

The use case I am talking about is when you have multiple components with inputs that will each display a data collection that is dynamic (pagination, search, etc…)

I handle it by definning the componnents params in the route’s model

  model() {
    const user=this.modelFor('users.edit').user
    return hash({
      user: user,
      paidParams: {'state': 'paids','user_id': user.id,},
      progressParams: {'state': 'progress','user_id':user.id},
      messages: {'user_id':user.id}
    });
  },

I pass each params to each component. In the component I define a loading function that is called in init() and also each time an input is made (here it’s pagination)

import Component from '@ember/component';
import { inject as service } from "@ember/service"

export default Component.extend({
  init(){
    this._super(...arguments);
    this.set('queryParams',Object.assign(this.get('queryParams'),{page:{number: 1,size: 5}}))
    this.get('loading')(this)
  },
  drawConsole: (function(){}),
  collection:null,
  queryParams:{},
  loading(parentThis){
    parentThis.set('isLoading',true)
    let queryParams=parentThis.get('queryParams')
    parentThis.set('collection',parentThis.get('store').query('invoice',queryParams)).then(
      ()=>{
        parentThis.set('isLoading',false)
      }
    );
  },
  store: service(),
  actions:{
    refreshData(page){
      let params = this.get('queryParams')
      params.page=Object.assign(params.page,{number: page})
      this.set('queryParams',params)
      this.get('loading')(this)
    }
  }
});

It works very well, but I never saw anyone use a JS function in an ember object, and It seems that access to the store for this type of data should be in the route itself.

For your use case, I think it makes sense to load data from the components, but I would suggest making the data loading functions into ember-concurrency tasks. Otherwise there are many concurrency bugs you’d have to solve manually. For example, the loading() method above:

  1. Is not cancelled if the component is torn down before the query finishes, which will result in an exception when set('isLoading', false) tries to run on a destroyed component.
  2. Allows multiple queries to race each other, if the input parameters change while a query is still running. The newest query doesn’t necessarily win, the fastest wins, and that could show inconsistent results.
  3. Gets stuck forever in the loading state if the query encounters a network error.

More detail on the general question of when and how to load data in the router vs components is here: This is relevant to your question:

Finally, I will point out that there’s no need to do this:

this.get('loading')(this)

You can invoke your own methods normally on Ember Component:

this.loading().

(Or use a Glimmer Component, which doesn’t extends from Ember.Object at all and has none of the special get, set, extend stuff that is causing this confusion.)

3 Likes