Ensure same data loaded for every route

I have an app where the template page header contains a drop-down list of about 2000 shops. When a User logged in, the list of shops he is assigned to is loaded in the above drop-down list and the first shop of the list is set as the currentShop via current-shop service. Other app routes use this currentShop instance to display some data (like a shop working hours, its address, etc.). So these routes use the currentShop id to make a request to fetch the shop’s data (address, working hours, etc.) as follows:

{+host}/shops/{shopId}/address
{+host}/shops/{shopId}/working-hours
...

The problem is that I fetch all the shops in the dashboard route to which the User is redirected once he is logged in:

# routes/dashboard.js

export default Route.extend(AuthenticatedRouteMixin, {
  currentUser: service(),
  currentShop: service(),
  shopService: service(),

  model() {
     return this.get('shopService').loadShops();
  }
});

where the shop-service looks like that:

# services/shop-service.js

export default Service.extend({
  currentShop:    service(),
  store: service(),
  shops: [],

  loadShops() {
    return this.store.findAll('shop').then(shops => {
      this.set('shops', shops);
      this.get('currentShop').setShop(shops.get('firstObject'));
    });
  }
});

and other routes just use the currentShop in their model hook to fetch data:

# routes/working-hours.js

model() {
    return this.store.query('working-hour', { shop_identifier: this.get('currentShop.shop.identifier')});
}

The error comes in case if I hit the reload the page (e.g. CMD-R) or reload the page button in the browser. In this case, I don’t have currentShop available because it is loaded in the dashboard route. How to guarantee the load/presence of the current shop for all the routes without duplicating the same call to shop-service#loadShops?

I moved the call to this.get('shopService').loadShops(); from the dashboard route to application route:

# routes/application.js

  beforeModel() {
    let locale = this.figureOutLocale();
    this.intl.setLocale(locale);
    moment.locale(locale);
    return this._loadCurrentUser();
  },
  
  model() {
    return this.get('shopService').loadShops();
  },
 async sessionAuthenticated() {
    let _super = this._super;
    await this._loadCurrentUser();
    _super.call(this, ...arguments);
  },

  _loadCurrentUser() {
    return this.get('currentUser').load().catch(() => this.get('session').invalidate());
  },
...

ans it seems to work as needed. But I lost the spinner displayed when waiting for the data fetched, - the page is white and empty until the data fetched. Any idea on how to improve that?

I think your solution makes a lot of sense. The only other thing you could do is nest all routes that need current shop under a new route that simply loads shops/current shop and renders an outlet. Something like:

route('shop', { path: 'shops/:shopid' }, function() {
  route('address');
  route('working-hours');
});

So your “shop” route blocks rendering of itself and child routes while shops are loading.

The only real difference between that and the application route is that then the application route could still render itself before the data is fetched.

You could also just use a loading template (like just put your loading spinner in app/templates/loading.hbs) that will render when the application route is still loading.

Thank you @dknutsen very much. I’ve completely lost it from the - a good idea to nest the shop related routes under the shop. I think this will make it possible to process the currentShop issue easier . As for the loading template, I already have it:

<div class="loading-pane">
  <div class="loading-message">
    {{t "main.loading.text"}}
    <div class="spinner"></div>
  </div>
</div>

but have no idea why it is not displayed at all while waiting for user authentification and shops data fetch to be done. The spinner worked fine when I loaded the shops in the dashboard route as it is the route I redirect to after the user is authenticated:

# routes/index.js

export default Route.extend(UnauthenticatedRouteMixin, {
  session:      service('session'),
  routeIfAlreadyAuthenticated: 'dashboard',
...

I’ll make the routes nested and come back with updates. By the way, I suppose that I’ll have to create shops folder and move the nested route files inside it when defining the nested routes as follows in router.js:

 route('shops', { path: ':shopId' }, function() {
    this.route('address');
   this.route('working-hours');
  ...

Right?

1 Like

yes, the file system should match your route names/nesting. I personally would recommend calling it “shop” instead of “shops” since the shopid param implies that it’s a single shop, but that’s just me :grin:. Also note that with nested routes you have an implicit ‘index’ route at each level so you’ll either want to define that or do a redirect to one of the other child routes.

1 Like

OK, thanks again for your time. I prefer to follow

a Rails way for routes, i.e. to have the parent route in plural:

Finally, I defined the follow routes: index -> application -> shops -> shop -> shop-related-routes:

# router.js

Router.map(function() {
  this.route('auth-error');
  this.route('callback');

  this.route('shops', function() {
    this.route('shop', { path: '/:shopId' }, function() {
      this.route('dashboard');
      this.route('address');
      this.route('client-relation');
...

I also moved all the related controllers and templates to ensure the same routes hierarchy. When starting the app, I’m getting:

Error while processing route: shops.index Could not find module `my-app/routes/mixins/authenticated-route-mixin` imported from `my-app/routes/shops/shop` 

What a I missing? Should I define explicitly shops/index.js route ?

I defined routes/shops.js as follows:

import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from '../mixins/authenticated-route-mixin';
import { inject as service } from '@ember/service';

export default Route.extend(AuthenticatedRouteMixin, {
  currentUser:   service(),

  model() {
    return this.store.findAll('shop', { reload: true });
  },

  afterModel(model, transition) {
    if (model.get('length') > 0) {
      this.transitionTo('shops.shop.dashboard', model.get('firstObject'));
    }
  }
});

andn changed index.js route to redirect to shops routes after authentication:

# routes/index.js

export default Route.extend(UnauthenticatedRouteMixin, {
  session:      service('session'),
  routeIfAlreadyAuthenticated: 'shops',
...

I changed the import from:

import AuthenticatedRouteMixin from '../mixins/authenticated-route-mixin';

to

import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

and the error has gone. Now there is another one:

Error: You must provide param `shopId` to `generate`.

it seems like it is missing in shops.js route afterModel hook:

# routes/shops.js

afterModel(model, transition) {
    if (model.get('length') > 0) {
      this.transitionTo('shops.shop.dashboard', model.get('firstObject'));
    }
  }

How can I pass shopId in this case?

It seems to work when defining the route as follows:

# routes/ shops.js

model() {
    return this.store.findAll('shop', { reload: true });
  },

  afterModel(model, transition) {
    if (model.get('length') > 0) {
      this.transitionTo('shops.shop.dashboard', model.get('firstObject').get('identifier'));
    }
  }
1 Like