The previous lesson used reactive controllers to solve the reuse of logic with its own state and lifecycle, like <task-card>'s deadline-proximity timer. But not every reusable behavior fits well into that mold: sometimes what you want to share across several components isn't an independent object with its own internal state, but rather properties and methods that should become part of the class itself and of the component's own public API, as if they had been written directly in it. For that second case, JavaScript offers a more general technique that predates Lit: mixins. This lesson explains the pattern, applies it to a small TaskFlow example, and closes with the criterion for deciding, in any future situation, between a mixin and a reactive controller.

Contents

  1. What a mixin is in plain JavaScript
  2. The typical mixin pattern in Lit
  3. Applying a mixin: ConEstadoCarga
  4. Using the mixin in a TaskFlow component
  5. Mixin versus reactive controller: the decision criterion
  6. Mixin limitations: composition order
  7. Mixin limitations: name collisions
  8. Wrap-up of module 6

  1. What a mixin is in plain JavaScript

A mixin, in JavaScript, is not a language keyword or a special feature: it is, simply, a function that receives a class as an argument and returns a new class that extends the one received, adding extra properties or methods to it. There's nothing Lit-specific about this idea; it's a general class-composition technique that has existed in JavaScript ever since classes themselves (class) became part of the language, taking advantage of the fact that extends can receive any expression that evaluates to a class, not just a literally written class name.

const MiMixin = (ClaseBase) => class extends ClaseBase {
  metodoNuevo() {
    console.log('Este método viene del mixin');
  }
};

class ClaseOriginal {
  metodoOriginal() {
    console.log('Este método viene de la clase original');
  }
}

class ClaseFinal extends MiMixin(ClaseOriginal) {}

const instancia = new ClaseFinal();
instancia.metodoOriginal(); // "Este método viene de la clase original"
instancia.metodoNuevo();    // "Este método viene del mixin"

ClaseFinal extends the result of calling MiMixin(ClaseOriginal), which is itself a new class (anonymous, defined with class extends ClaseBase { ... } inside the function body) that inherits from ClaseOriginal and adds metodoNuevo to it. The final result has access both to what already existed in ClaseOriginal and to what the mixin contributes, exactly as if a single class had been written with everything together, while still keeping MiMixin as a separate piece reusable with any other base class.

  1. The typical mixin pattern in Lit

Applied to Lit components, the pattern is identical, with the particularity that the "base class" the mixin receives is usually, ultimately, LitElement (or the result of already applying another mixin on top of LitElement):

const MiMixin = (Base) => class extends Base {
  static properties = {
    ...Base.properties,
    propiedadNueva: { type: String },
  };

  constructor(...args) {
    super(...args);
    this.propiedadNueva = 'valor por defecto';
  }
};

class MiComponente extends MiMixin(LitElement) {
  render() {
    return html`<p>${this.propiedadNueva}</p>`;
  }
}

Two details of this pattern deserve attention before applying it to a real example. First, static properties = { ...Base.properties, propiedadNueva: {...} }: since static properties is a normal JavaScript object, you must explicitly combine, with the spread operator, the properties already declared by the base class with the new ones the mixin adds; forgetting ...Base.properties would make any reactive property declared further down the inheritance chain (for example, directly in MiComponente, if it in turn extended static properties) stop working correctly, because MiMixin would have overwritten it with an object that doesn't include it. Second, the constructor(...args) { super(...args); ... }: a mixin must faithfully forward any arguments it receives to super(...args), because it cannot know in advance what arguments the base class it will be applied to expects (in LitElement's case, its own constructor doesn't usually take arguments, but a mixin shouldn't assume that if it aims to be reusable with any base class).

  1. Applying a mixin: ConEstadoCarga

TaskFlow doesn't have, for now, any operation that involves a visible wait (like a network request; that will arrive in module 8), but it serves as a clear, self-contained example of a behavior several of the application's components might need to share in the near future: a cargando property and a reusable way to wrap a template with a visual indicator while that property is active.

// src/mixins/con-estado-carga.js
import { html } from 'lit';

export const ConEstadoCarga = (Base) => class extends Base {
  static properties = {
    ...Base.properties,
    cargando: { state: true },
  };

  constructor(...args) {
    super(...args);
    this.cargando = false;
  }

  conIndicadorDeCarga(plantilla) {
    if (this.cargando) {
      return html`<p class="cargando">Cargando…</p>`;
    }
    return plantilla;
  }
};

ConEstadoCarga adds two things to any class it's applied to: the state property cargando (initialized to false), and the method conIndicadorDeCarga(plantilla), which receives the component's "normal" template and returns, in its place, a loading notice if cargando is true. Notice that, unlike the previous lesson's reactive controller, there is no separate object here: cargando becomes just another reactive property of the final class itself, as accessible as titulo or estado on <task-card>, and conIndicadorDeCarga becomes just another method of that same class, callable as this.conIndicadorDeCarga(...) from inside render().

  1. Using the mixin in a TaskFlow component

Applying the mixin to a component is as direct as wrapping LitElement in the function call:

// src/components/task-board.js
import { LitElement, html, css } from 'lit';
import { ConEstadoCarga } from '../mixins/con-estado-carga.js';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './task-list.js';

class TaskBoard extends ConEstadoCarga(LitElement) {
  static properties = {
    ...ConEstadoCarga(LitElement).properties,
    tareas: { type: Array },
  };

  // ...constructor, gestionarTareaCambiada(), and styles unchanged...

  render() {
    return this.conIndicadorDeCarga(html`
      <div class="tablero">
        <h1>TaskFlow</h1>
        <task-list .tareas="${this.tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
      </div>
    `);
  }
}

customElements.define('task-board', TaskBoard);

TaskBoard extends ConEstadoCarga(LitElement) instead of LitElement directly, and from that point on it has access, as if they had always been its own, to both the cargando property and the conIndicadorDeCarga method. TaskBoard's own render() wraps its usual template in a call to this.conIndicadorDeCarga(...): as long as this.cargando is false (its default value), the behavior is identical to what it was before applying the mixin; as soon as some code sets this.cargando = true (for example, when starting an operation that takes a while to complete), render() will show the loading notice in its place, without TaskBoard having had to write that conditional logic itself.

It's worth noting the somewhat repetitive construction static properties = { ...ConEstadoCarga(LitElement).properties, tareas: {...} }: as explained in section 2, every level of the chain that adds its own reactive properties must remember to also propagate those inherited from the previous level, and this includes the final class itself that uses the mixin, not just the mixin on its own. It's a maintenance detail to keep in mind every time a mixin is combined with properties of the final component.

  1. Mixin versus reactive controller: the decision criterion

With both techniques now covered in this module, it's worth settling on a clear criterion for choosing between them, rather than applying them interchangeably without reasoning about why:

Criterion Mixin Reactive controller
Where does the added state live? Directly on the component instance (this.cargando) In its own object, separate from the host (this._contadorTiempo.cercaDeVencer)
Does it become part of the component's public API? Yes: its properties and methods become indistinguishable from the component's own Not necessarily: the host decides what to expose, if anything
How is it activated? By wrapping the class with extends MiMixin(Base) By instantiating it inside the constructor with new Controlador(this)
Best use case Behavior that should feel like part of the class itself (a utility method, a property the rest of the component uses naturally) Logic with its own state and lifecycle needs, meant to remain decoupled from the host
Example from this course ConEstadoCarga: cargando and conIndicadorDeCarga feel like a natural part of TaskBoard ContadorTiempoRestanteController: the timer doesn't need to merge with TaskCard's public API

The underlying criterion, summed up in one sentence, is this: a mixin is appropriate when the behavior should be integrated into the class itself and its public API, as if it had been written there directly; a reactive controller is preferable when the logic has its own internal state and it's worth keeping it decoupled, like a piece the host uses but doesn't need to inherit from or expose its details directly. Lit's official documentation, in fact, recommends reactive controllers as the first option over mixins in most cases involving stateful logic, precisely because of the problems explained in the following two sections.

  1. Mixin limitations: composition order

When a single mixin is applied, as in the ConEstadoCarga example, the order creates no ambiguity. The problem appears as soon as several mixins are combined on the same base class:

class TaskBoard extends MixinA(MixinB(LitElement)) {}

Here, MixinB is applied first on LitElement, and MixinA is applied afterward on the result of MixinB(LitElement). If both mixins override, say, the same lifecycle method (connectedCallback, or any other), the nesting order determines which of the two versions "sees" the call first and which depends on the other correctly invoking super so as not to lose its own behavior. With two mixins, this reasoning already requires some care; with three or more, applied in different orders across different components of the same application, the result can become hard to predict without carefully reading the code of every mixin involved — something that rarely happens with a single reactive controller (or with several, registered independently via addController, with no inheritance-order relationship between them).

  1. Mixin limitations: name collisions

The second frequent problem with mixins is name collisions: if two different mixins, applied on the same class, declare a property or method with the same name (or if a mixin unknowingly uses a name the final class already used on its own), one of the two silently overwrites the other, with no warning or error from JavaScript. For example, if a second TaskFlow mixin, meant to handle errors, also declared a property called cargando (perhaps with a slightly different meaning), and it were combined with ConEstadoCarga on the same component, one of the two cargando values would prevail over the other depending on the application order, and the result would be hard to debug without knowing the internal code of both mixins.

This risk is, precisely, one of the main reasons a reactive controller tends to be safer: since its state lives in its own object (this._contadorTiempo, not directly on this), two different controllers can never collide with each other or with the host's own properties, even if they use identical field names internally, because each one lives in its own namespace, isolated from the rest.

  1. Wrap-up of module 6

This lesson completes module 6. The journey has gone from less to more control over a component's lifecycle: first the callbacks inherited from Custom Elements (connectedCallback, disconnectedCallback), then Lit's own hooks for its update cycle (willUpdate, firstUpdated, updated, updateComplete), and finally two composition techniques for reusing all that logic across several components without duplicating it: reactive controllers, recommended when the behavior has its own state and it's worth keeping it decoupled, and mixins, suitable when the behavior needs to integrate naturally into the class itself and its public API, at the cost of taking on the risk of name collisions and a composition order that can become hard to reason about with several mixins combined.

With the lifecycle now mastered, the course turns its attention to different territory: templates. Module 7, "Directives and Advanced Template Features," will present a set of tools —directives such as classMap, styleMap, or until, among others— that, in more than one case, allow simplifying patterns this very course has already solved by hand up to now with explicit JavaScript code, exactly the kind of simplification that is better appreciated once you understand, as is already the case from this module onward, what really happens underneath when a template gets re-rendered.

Common Mistakes and Tips

  • Forgetting to propagate Base.properties when declaring static properties inside a mixin (or in the final class that uses it): as seen in sections 2 and 4, without ...Base.properties any reactive property declared at a different level of the mixin chain stops being registered as reactive, producing silent bugs where a property "doesn't react" with no visible error message.
  • Chaining too many mixins onto the same component: as explained in section 6, each additional mixin increases the difficulty of reasoning about execution order and possible collisions; if a component starts needing three or four combined mixins, it's usually a sign that at least part of that logic would fit better as independent reactive controllers.
  • Using a mixin for stateful logic that doesn't need to integrate into the component's public API: as explained in section 5, if the behavior (like the previous lesson's timer) can live perfectly decoupled, without the rest of the component needing to treat it as one of its own properties or methods, a reactive controller entirely avoids the risks of name collisions and composition order.
  • Not documenting what a mixin expects from its base class: if ConEstadoCarga assumed, for example, the existence of a render() method with a specific shape (beyond receiving its result as the argument to conIndicadorDeCarga), any component using it would need to know that implicit contract; the clearer and more minimal what a mixin demands from its base, the easier it will be to reuse it safely in future components.

Exercises

  1. Write a second mixin, ConContadorDeErrores, that adds a state property ultimoError (initialized to null) and a method registrarError(mensaje) that updates it. Apply it together with ConEstadoCarga on TaskBoard (class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement))), and check that both properties (cargando and ultimoError) coexist without problems, since they don't share any name.
  2. Explain, based on section 7, what would happen if ConContadorDeErrores from the previous exercise also declared a property called cargando (for example, to indicate whether an operation is being retried after an error), and which application order of the two mixins would make each value prevail.
  3. Revisit the previous lesson's ContadorTiempoRestanteController and explain, in your own words, why it wouldn't make sense to rewrite it as a mixin (ConContadorDeTiempoRestante = (Base) => class extends Base {...}) applied directly on TaskCard, drawing on the criterion from section 5.

Solutions

// src/mixins/con-contador-de-errores.js
export const ConContadorDeErrores = (Base) => class extends Base {
  static properties = {
    ...Base.properties,
    ultimoError: { state: true },
  };

  constructor(...args) {
    super(...args);
    this.ultimoError = null;
  }

  registrarError(mensaje) {
    this.ultimoError = mensaje;
  }
};
class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)) {
  static properties = {
    ...ConContadorDeErrores(ConEstadoCarga(LitElement)).properties,
    tareas: { type: Array },
  };
}

Since cargando and ultimoError are different names, both mixins add their properties with no conflict, and TaskBoard ends up with access to this.cargando, this.conIndicadorDeCarga(...), this.ultimoError, and this.registrarError(...), all available simultaneously.

  1. If both mixins declared a cargando property, the mixin applied last (the outermost in the nesting, that is, the first one reading the expression from left to right) would be the one to define the final version of static properties.cargando and, in its constructor, the last assignment of this.cargando = ... to run (since each mixin calls super(...args) before assigning its own default value, the outermost mixin runs after the chain of super calls, and its assignment is the one left in effect once construction finishes). In class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)), it would be ConContadorDeErrores that prevails, and the original meaning of cargando contributed by ConEstadoCarga would be silently overwritten, with no error warning.

  2. ContadorTiempoRestanteController keeps internal state (the interval's identifier, the current value of cercaDeVencer) that never needs to feel like part of TaskCard's public API; the rest of the component only needs to read this._contadorTiempo.cercaDeVencer from render(), without cercaDeVencer having to be just another reactive property of TaskCard on equal footing with titulo or estado. Turning it into a mixin would force merging that state directly into TaskCard (as happens with cargando in ConEstadoCarga), increasing the risk of name collisions if, in the future, TaskCard or another mixin applied on it also needed a property called cercaDeVencer or similar; in addition, the mixin would also inherit the composition-order problem from section 6 as soon as it were combined with any other future mixin, something a reactive controller, isolated in its own object, avoids entirely.

Conclusion

This module has explained in detail the complete lifecycle of a Lit component: the callbacks inherited from Custom Elements, Lit's own update-cycle hooks, and two composition techniques —reactive controllers and mixins— for reusing behavior across components without duplicating code. ConEstadoCarga, this final lesson's mixin, has shown the case where merging behavior directly into a component's class makes sense, and the comparison with ContadorTiempoRestanteController has left a clear criterion for deciding, in any future TaskFlow situation, between one technique and the other.

With the lifecycle now mastered, it's time to look at advanced template features (directives) that simplify patterns already seen.

Lit Course

Module 1: Introduction to Lit and Web Components

Module 2: Reactive Templates and Rendering

Module 3: Reactive Properties and State

Module 4: Styling Lit Components

Module 5: Events and Component Communication

Module 6: Lifecycle and Advanced Behavior

Module 7: Directives and Advanced Template Features

Module 8: Integration, Interoperability and Deployment

Module 9: Testing and Best Practices

Module 10: Project: Building TaskFlow

© Copyright 2026. All rights reserved