How to replace mixins for large controllers or routes

Hi,

I know that mixins are not recommended any more, but I find them especially useful for breaking up large sections of code for the same controller or route. For example a section that deals with billing is all put inside one mixin and another section that deals with settings is all put inside another mixin. I find this a great way to logically separate a controller that may otherwise turn too big to read/understand/edit later.

Is there a way to have the same “convenience” but without prompting errors by Ember?

Thanks :slight_smile:

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

A quick addendum, picking up on a note I said above but didn’t make explicit: if those newly-introduced classes and functions won’t be shared, just leave them in the same module!

import Controller from '@ember/controller';

export default class MyController extends Controller {
  constructor() {
    super(...arguments);
    this.shared = new SharedStuff();
    this.more = new MoreStuff();
    this.yetMore = new YetMoreStuff();
  }
}

export class SharedStuff {
  // ...
}

export class MoreStuff {
  // ...
}

export class YetMore {
  // ..
}

Here these are named exports, so you could pull them into unit tests and test them that way, but with an eye to otherwise only using them in this module.