Best way to replace Mixins

The short answer is that there isn’t one best way to replace mixins. In fact, so far as I’ve worked out in prepping the app I work on to migrate, there are five basic patterns for successfully migrating away from mixins: services, standalone classes, standalone functions, class decorators, and inheritance. Which is the best to use depends on what the existing mixin does. The last two, class decorators and inheritance, should be your options of last resort in my experience.

When you should choose each:

  • If the mixin is a way of sharing long-lived state, extract to a service and inject it wherever it was being used before. This is, as you note, sometimes painful—but in my experience that pain tends to highlight bad design decisions and over-coupling you want to undo anyway; this is a good opportunity for refactoring.

    In my experience, this is also pretty rare, though: if you actually needed long-lived state, you very probably already had a service. This is the thing people (myself in the past included!) tend to reach for first, but it’s rarely the right choice.

  • If the mixin is a way of supplying non-shared state or behavior, only meant to live for as long as whatever it’s mixed into, you can replace it with another class which is simply instantiated at the time the class is constructed. This is a much more common pattern, and this solution is a fairly standard one in traditional object-oriented solutions: it’s delegation.

    The class you use for this does not need to (and should not) be an EmberObject subclass; it should nearly always just be a normal ES6 base class itself.

    When doing this, you’ll just import the class from the module it lives in and set it up on your class:

    import Component from '@glimmer/component';
    import UtilityClass from 'my-app/lib/utility-class';
    
    export default class MyComponent extends Component {
      constructor() {
        super(...arguments);
        this.utility = new UtilityClass(this.args.useful);
      }
    }
    
    {{this.utility.someThing}}
    

    As with the service-oriented refactor, this does entail a lot of change through your app. However, it’s also straightforward, largely mechanical. With both the service approach and this one, you can likely write a codemod for it.

    (You can also make this configurable if you need to, by registering the class with the DI system. In practice, it’s rare that you need to do that, and I would recommend against starting with that. Do it only if you find you actually need it!)

  • When the mixin is only supplying shared behavior—no state—you can extract it to utility functions. Those functions can just be imports from a module; people with long Ember experience or coming from Java or C# backgrounds tend to reach for classes as namespaces, and you can do that but you really don’t need to. import { someHelper } from 'my-app/lib/fun-helpers' works just fine, and is more tree-shaking-friendly (in a Coming Soon™ tree-shaking world) to boot!

  • Very rarely, you’ll find that what you really want is to add functionality or data to an existing class without using one of the above methods—presumably because you take it to be too much boilerplate. Here, you can (but still probably shouldn’t!) use a class decorator. This is basically the ES6 class equivalent of a mixin, and because it’s a way of rewriting the class internals to add behavior or data, it has many of the same pitfalls that mixins do.

    That includes the fact that if you use more than one class decorator (as many people use many mixins) the order they’re applied matters, and you will lose visibility into where the behavior and state actually exist. The spec is also still in flux, three years along, and so you’re taking on extra risk if you write your own decorators: this is something that you will have to change later.

    My personal take is that decorators should almost never be the solution app code reaches for, and library code (i.e. addons, whether internal or open source) should reach for them only very, very rarely.

  • Equally rarely, you may find you want to reach for inheritance to share the same set of behavior and data fields among classes. Long experience in the industry has shown that inheritance is rarely the right way to manage this, though, and that you’re usually better reaching for a mix of DI (with services), delegates (with regular classes), and utility functions.

For a bit more detail (including code samples on some of these scenarios) you may find @pzuraq’s post Do You Need EmberObject helpful reading.

13 Likes