How to replace mixins for large controllers or routes

Hey @everydaypanos, great question! I covered the broad outline of strategies for moving away from mixing in an earlier discussion here, so I’d recommend that as a good initial read.

For the specific use case you’re outlining here, I’d recommend taking those mixins and turning them into a mix of standalone native classes and standalone functions—whether in the same module, or in utility modules you can import them from (both are totally reasonable!).

  • Often, shared functionality can be extracted to standalone functions, with arguments passed in. A lot of the mixins I see for shared behavior don’t really need to be attached to a class in any way; they just need to have data handed in and to hand data back out.

  • For cases where you have shared state, you can break it out into classes. (They can even be in the same module if that’s helpful!) Then you can instantiate those in the controller’s constructor.

Even if you only used that second pattern—extracting to smaller utility classes—you get a number of benefits, including the code usually becoming easier to understand and maintain and test, because each of those classes can really focus on one responsibility, and the controller itself just becomes a point for coordinating those, rather than having to manage it all at once in a single context. Layering that together with extracting functions where they don’t actually need to update state (“pure” functions, so called because they are purely a transformation from input to output) gives you an even better improvement.

The one “downside” is that you’ll have a slight increase in indirection:

  • With class extraction, instead of this.someMixinMethod() you’ll end up with this.otherClass.someMethod().
  • With function extraction, instead of this.someMixinMethod() you’ll end up with someFunction(this.foo, this.bar).

Personally, I find that tradeoff to be more than worth it, and in fact with tools which can use the TypeScript language service (which does not require using TS itself at all!), you’ll get more helpful autocomplete.

In practice, you’ll end up taking something like this:

import SharedStuff from 'my-app/mixins/shared-stuff';
import MoreStuff from 'my-app/mixins/more-stuff';
import YetMore from 'my-app/mixins/yet-more';

export default Controller.extend(SharedStuff, MoreStuff, YetMore, {
  // normal class definition
});
// shared-stuff.js
export default Mixin.extend({
  // ...
})
// more-stuff.js
export default Mixin.extend({
  // ...
})
// yet-more.js
export default Mixin.extend({
  // ...
})

…and turning it into something like this:

import SharedStuff from 'my-app/lib/shared-stuff';
import MoreStuff from 'my-app/lib/more-stuff';
import YetMore from 'my-app/lib/yet-more';

export default class MyController extends Controller {
  constructor() {
    super(...arguments);
    this.shared = new SharedStuff();
    this.more = new MoreStuff();
    this.yetMore = new YetMoreStuff();
  }
}
// shared-stuff.js
export default class SharedStuff {
  // ...
}
// more-stuff.js
export default class MoreStuff {
  // ...
}
// yet-more.js
export default class YetMore {
  // ...
}
1 Like