Mixins

by Julian Rubisch

Bad

// overlay_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  showOverlay(e) {
    // ...
  }

  hideOverlay(e) {
    // ...
  }
}

// dropdown_controller.js
import OverlayController from "./overlay_controller";

export default class extends OverlayController {
  //...
}

// flyout_controller.js
import OverlayController from "./overlay_controller";

export default class extends OverlayController {
  //...
}

Good

// mixins/useOverlay.js
export const useOverlay = controller => {
  Object.assign(controller, {
    showOverlay(e) {
      // ...
    },
    hideOverlay(e) {
      // ...
    }
  });
};

// dropdown_controller.js
import { useOverlay } from "./mixins/useOverlay";

export default class extends Controller {
  connect() {
    useOverlay(this);
  }

  //...
}

// flyout_controller.js
import { useOverlay } from "./mixins/useOverlay";

export default class extends Controller {
  connect() {
    useOverlay(this);
  }

  //...
}

Rationale

Stimulus controllers are meant to be used as mixins themselves (i.e. applying multiple controllers to one DOM element, thus mixing in behavior). Sometimes though, it might even be advisable to share behavior on the controller level. Inheritance isn’t always the answer to this - more often than not, it leads to architectural issues down the road, e.g. when you discover that you need to inherit behavior from two different parents - which is impossible in JavaScript. Often, it’s preferable to mix in behavior that can be used across controllers with a simple trick.

If you’re unsure about whether inheritance or mixin is the correct pattern, ask yourself:

Counterindications

Mixins might still not be the right choice, composition might suit your needs even better. Is what you’re modelling a trait (acts as a), or a collaborator (has a)? Use composition, i.e. model your collaborator as a separate JS module or class and instantiate it in the connect method!

References