Routes have a responsibility to fetch data; it is natural to also think they are responsible for managing that data (Create Read, Update, Delete). This has lead to an anti-pattern of Route Actions. The way Routes in Ember are implemented forces some work-arounds to access the modification methods (actions).

We had bubbling actions (controller sends and action which bubbles to the route) and we had ember-route-action-helpers which side stepped the route/controller boundary to access actions defined on the route.

Both of these solutions when used at scale increased the indirection and introduced tight coupling instead of relying on an abstraction to communicate between objects Route Actions expose implementation details to external objects violating several S.O.L.I.D. principles.

Example

// Route
export default class MyRoute extends Route {
  model() {
    // ...
  }
  @action refresh() {
    this.refresh();
  }
  @action saveModel() {
    // ...
  }
  @action deleteModel() {
    // ...
  }
  @action createModel() {
    // ...
  }
}

// Controller
export default class MyController extends Controller {
  @action refresh() {
    this.send('refresh');
  }
  @action saveModel() {
    this.send('saveModel');
  }
  @action deleteModel() {
    this.send('deleteModel');
  }
  @action createModel() {
    this.send('createModel');
  }
}

There are several alternative designs and we will cover a few of them here. An example is ember-data which provides a service that can be used outside of the Route. It also enables individual model objects to manage themselves.

Potential Solution: Manager Pattern

In cases where ember-data pattern is not in use you can still hold data management in the Route but have the Route offer the controller a Manager. This Manager is an interface that the Route and Controller can agree on. It is a bridge between the two which each can rely on. It is something which can be codified either through Duck Typing or as a fully fleshed out Class.

export default MyRoute extends Route {
  setupController(controller, model) {
    super.setupController(...arguments);
    controller.registerModelManager({
      refresh: () => this.refresh(),
      create: (newModel) => this.createModel(newModel),
      delete: () => this.deleteModel(model),
      save: () => this.saveModel(model)
    });
  }
}
export default MyRoute extends Route {
  setupController(controller, model) {
    super.setupController(...arguments);
    controller.registerModelManager(new MyModelManager(model));
  }
}

Potential Solution: Service

Instead of defining everything on the route, a service can be in charge of managing the data and api requests — similar to what ember-data does.

import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';

export default class Posts extends Service {
  @tracked recordCache = new TrackedArray([]);

  findRecord(id) {
    // fetch(...)
    this.recordCache.push(record);

    return record;
  }

  update(id, data) {
    // fetch(...)
  }
}