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
- What a mixin is in plain JavaScript
- The typical mixin pattern in Lit
- Applying a mixin:
ConEstadoCarga - Using the mixin in a TaskFlow component
- Mixin versus reactive controller: the decision criterion
- Mixin limitations: composition order
- Mixin limitations: name collisions
- Wrap-up of module 6
- 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.
- 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).
- Applying a mixin:
ConEstadoCarga
ConEstadoCargaTaskFlow 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().
- 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.
- 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.
- 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:
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).
- 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.
- 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.propertieswhen declaringstatic propertiesinside a mixin (or in the final class that uses it): as seen in sections 2 and 4, without...Base.propertiesany 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
ConEstadoCargaassumed, for example, the existence of arender()method with a specific shape (beyond receiving its result as the argument toconIndicadorDeCarga), 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
- Write a second mixin,
ConContadorDeErrores, that adds a state propertyultimoError(initialized tonull) and a methodregistrarError(mensaje)that updates it. Apply it together withConEstadoCargaonTaskBoard(class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement))), and check that both properties (cargandoandultimoError) coexist without problems, since they don't share any name. - Explain, based on section 7, what would happen if
ConContadorDeErroresfrom the previous exercise also declared a property calledcargando(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. - Revisit the previous lesson's
ContadorTiempoRestanteControllerand explain, in your own words, why it wouldn't make sense to rewrite it as a mixin (ConContadorDeTiempoRestante = (Base) => class extends Base {...}) applied directly onTaskCard, 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.
-
If both mixins declared a
cargandoproperty, 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 ofstatic properties.cargandoand, in itsconstructor, the last assignment ofthis.cargando = ...to run (since each mixin callssuper(...args)before assigning its own default value, the outermost mixin runs after the chain ofsupercalls, and its assignment is the one left in effect once construction finishes). Inclass TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)), it would beConContadorDeErroresthat prevails, and the original meaning ofcargandocontributed byConEstadoCargawould be silently overwritten, with no error warning. -
ContadorTiempoRestanteControllerkeeps internal state (the interval's identifier, the current value ofcercaDeVencer) that never needs to feel like part ofTaskCard's public API; the rest of the component only needs to readthis._contadorTiempo.cercaDeVencerfromrender(), withoutcercaDeVencerhaving to be just another reactive property ofTaskCardon equal footing withtituloorestado. Turning it into a mixin would force merging that state directly intoTaskCard(as happens withcargandoinConEstadoCarga), increasing the risk of name collisions if, in the future,TaskCardor another mixin applied on it also needed a property calledcercaDeVenceror 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
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
