La lección anterior ha resuelto, con controladores reactivos, la reutilización de lógica con estado propio y ciclo de vida, como el temporizador de proximidad de fecha de <task-card>. Pero no todo comportamiento reutilizable encaja bien en ese molde: a veces lo que se quiere compartir entre varios componentes no es un objeto independiente con su propio estado interno, sino propiedades y métodos que deberían pasar a formar parte de la propia clase y de la propia API pública del componente, como si se hubieran escrito directamente en ella. Para ese segundo caso, JavaScript ofrece una técnica más general y anterior a Lit: los mixins. Esta lección explica el patrón, lo aplica a un pequeño ejemplo de TaskFlow, y cierra con el criterio para decidir, en cada situación futura, entre un mixin y un controlador reactivo.

Contenido

  1. Qué es un mixin en JavaScript puro
  2. El patrón típico de un mixin en Lit
  3. Aplicando un mixin: ConEstadoCarga
  4. Usando el mixin en un componente de TaskFlow
  5. Mixin frente a controlador reactivo: el criterio de decisión
  6. Limitaciones de los mixins: orden de composición
  7. Limitaciones de los mixins: colisión de nombres
  8. Cierre del módulo 6

  1. Qué es un mixin en JavaScript puro

Un mixin, en JavaScript, no es una palabra clave del lenguaje ni una característica especial: es, sencillamente, una función que recibe una clase como argumento y devuelve una nueva clase que extiende a la recibida, añadiéndole propiedades o métodos adicionales. No hay nada específico de Lit en esta idea; es una técnica general de composición de clases que existe en JavaScript desde que las clases mismas (class) forman parte del lenguaje, aprovechando que extends puede recibir cualquier expresión que evalúe a una clase, no solo un nombre de clase escrito literalmente.

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 extiende el resultado de llamar a MiMixin(ClaseOriginal), que es, en sí mismo, una clase nueva (anónima, definida con class extends ClaseBase { ... } dentro del cuerpo de la función) que hereda de ClaseOriginal y le añade metodoNuevo. El resultado final tiene acceso tanto a lo que ya existía en ClaseOriginal como a lo que aporta el mixin, exactamente como si se hubiera escrito una única clase con todo junto, pero manteniendo MiMixin como una pieza separada y reutilizable con cualquier otra clase base.

  1. El patrón típico de un mixin en Lit

Aplicado a componentes de Lit, el patrón es idéntico, con la particularidad de que la "clase base" recibida por el mixin suele ser, en último término, LitElement (o el resultado de aplicar ya otro mixin sobre 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>`;
  }
}

Dos detalles de este patrón merecen atención antes de aplicarlo a un ejemplo real. Primero, static properties = { ...Base.properties, propiedadNueva: {...} }: como static properties es un objeto normal de JavaScript, hay que combinar explícitamente, con el operador de propagación, las propiedades ya declaradas por la clase base con las nuevas que aporta el mixin; olvidar el ...Base.properties haría que cualquier propiedad reactiva declarada más abajo en la cadena de herencia (por ejemplo, directamente en MiComponente, si extendiera static properties a su vez) dejara de funcionar correctamente, porque MiMixin la habría sobreescrito con un objeto que no la incluye. Segundo, el constructor(...args) { super(...args); ... }: un mixin debe reenviar fielmente cualquier argumento recibido a super(...args), porque no puede saber de antemano qué argumentos espera la clase base sobre la que se vaya a aplicar (en el caso de LitElement, el propio constructor no suele recibir argumentos, pero el mixin no debería asumirlo si aspira a ser reutilizable con cualquier clase base).

  1. Aplicando un mixin: ConEstadoCarga

TaskFlow no tiene, por ahora, ninguna operación que implique una espera visible (como una petición de red; eso llegará en el módulo 8), pero sirve como ejemplo claro y autocontenido de un comportamiento que varios componentes de la aplicación podrían necesitar compartir en un futuro cercano: una propiedad cargando y una forma reutilizable de envolver una plantilla con un indicador visual mientras esa propiedad esté activa.

// 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 añade dos cosas a cualquier clase sobre la que se aplique: la propiedad de estado cargando (inicializada a false), y el método conIndicadorDeCarga(plantilla), que recibe la plantilla "normal" del componente y devuelve, en su lugar, un aviso de carga si cargando es true. Nótese que, a diferencia del controlador reactivo de la lección anterior, aquí no hay ningún objeto separado: cargando pasa a ser una propiedad reactiva más de la propia clase final, tan accesible como titulo o estado en <task-card>, y conIndicadorDeCarga pasa a ser un método más de esa misma clase, invocable como this.conIndicadorDeCarga(...) desde dentro de render().

  1. Usando el mixin en un componente de TaskFlow

Aplicar el mixin a un componente es tan directo como envolver LitElement en la llamada a la función:

// 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() y estilos sin cambios...

  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 extiende ConEstadoCarga(LitElement) en lugar de LitElement directamente, y a partir de ese momento tiene acceso, como si fueran suyos desde siempre, tanto a la propiedad cargando como al método conIndicadorDeCarga. El propio render() de TaskBoard envuelve su plantilla habitual en una llamada a this.conIndicadorDeCarga(...): mientras this.cargando sea false (su valor por defecto), el comportamiento es idéntico al de antes de aplicar el mixin; en cuanto algún código active this.cargando = true (por ejemplo, al iniciar una operación que tarde un tiempo en completarse), render() mostrará el aviso de carga en su lugar, sin que TaskBoard haya tenido que escribir esa lógica condicional por sí misma.

Merece la pena notar la construcción, algo repetitiva, de static properties = { ...ConEstadoCarga(LitElement).properties, tareas: {...} }: como se explicó en el apartado 2, cada nivel de la cadena que añada sus propias propiedades reactivas debe acordarse de propagar también las heredadas del nivel anterior, y esto incluye a la propia clase final que usa el mixin, no solo al mixin en sí. Es un detalle de mantenimiento a tener presente cada vez que se combina un mixin con propiedades propias del componente final.

  1. Mixin frente a controlador reactivo: el criterio de decisión

Con las dos técnicas ya vistas en este módulo, conviene fijar un criterio claro para elegir entre ellas, en lugar de aplicarlas de forma intercambiable sin razonar el porqué:

Criterio Mixin Controlador reactivo
¿Dónde vive el estado añadido? Directamente en la instancia del componente (this.cargando) En un objeto propio, separado del host (this._contadorTiempo.cercaDeVencer)
¿Se integra en la API pública del componente? Sí: sus propiedades y métodos pasan a ser indistinguibles de los propios del componente No necesariamente: el host decide qué exponer, si es que expone algo
¿Cómo se activa? Envolviendo la clase con extends MiMixin(Base) Instanciándolo dentro del constructor con new Controlador(this)
Mejor caso de uso Comportamiento que debería sentirse parte de la propia clase (un método de utilidad, una propiedad que el resto del componente usa con naturalidad) Lógica con estado propio y necesidades de ciclo de vida, pensada para permanecer desacoplada del host
Ejemplo de este curso ConEstadoCarga: cargando y conIndicadorDeCarga se sienten parte natural de TaskBoard ContadorTiempoRestanteController: el temporizador no necesita fundirse con la API pública de TaskCard

El criterio de fondo, resumido en una frase, es este: un mixin es adecuado cuando el comportamiento debería integrarse en la propia clase y su API pública, como si se hubiera escrito ahí directamente; un controlador reactivo es preferible cuando la lógica tiene su propio estado interno y conviene mantenerla desacoplada, como una pieza que el host usa pero de la que no necesita heredar ni exponer directamente sus detalles. La documentación oficial de Lit se decanta, de hecho, por recomendar controladores reactivos como primera opción frente a los mixins en la mayoría de los casos de lógica con estado, precisamente por los problemas que se explican en los dos apartados siguientes.

  1. Limitaciones de los mixins: orden de composición

Cuando se aplica un único mixin, como en el ejemplo de ConEstadoCarga, el orden no genera ninguna ambigüedad. El problema aparece en cuanto se combinan varios mixins sobre la misma clase base:

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

Aquí, MixinB se aplica primero sobre LitElement, y MixinA se aplica después sobre el resultado de MixinB(LitElement). Si ambos mixins sobreescriben, por ejemplo, el mismo método del ciclo de vida (connectedCallback, o cualquier otro), el orden de anidamiento determina cuál de las dos versiones "ve" primero la llamada y cuál depende de que la otra invoque a super correctamente para no perder su propio comportamiento. Con dos mixins, este razonamiento ya exige cierto cuidado; con tres o más, aplicados en distintos órdenes en distintos componentes de una misma aplicación, el resultado puede volverse difícil de predecir sin leer con atención el código de cada mixin implicado, algo que rara vez ocurre con un único controlador reactivo (o con varios, registrados de forma independiente mediante addController, sin ninguna relación de orden de herencia entre ellos).

  1. Limitaciones de los mixins: colisión de nombres

El segundo problema frecuente de los mixins es la colisión de nombres: si dos mixins distintos, aplicados sobre la misma clase, declaran una propiedad o un método con el mismo nombre (o si un mixin usa, sin darse cuenta, un nombre que la propia clase final ya usaba por su cuenta), uno de los dos silenciosamente sobreescribe al otro, sin que JavaScript emita ningún aviso ni error. Por ejemplo, si un segundo mixin de TaskFlow, pensado para gestionar errores, también declarase una propiedad llamada cargando (quizá con un significado ligeramente distinto), y se combinara con ConEstadoCarga sobre el mismo componente, uno de los dos valores de cargando prevalecería sobre el otro según el orden de aplicación, y el resultado sería difícil de depurar sin conocer el código interno de ambos mixins.

Este riesgo es, precisamente, uno de los motivos principales por los que un controlador reactivo suele ser más seguro: como su estado vive en un objeto propio (this._contadorTiempo, no directamente this), dos controladores distintos nunca pueden colisionar entre sí ni con las propiedades del propio host, aunque usen internamente nombres de campo idénticos, porque cada uno vive en su propio espacio de nombres, aislado del resto.

  1. Cierre del módulo 6

Con esta lección se completa el módulo 6. El recorrido ha ido de menos a más control sobre el ciclo de vida de un componente: primero los callbacks heredados de Custom Elements (connectedCallback, disconnectedCallback), después los hooks propios del ciclo de actualización de Lit (willUpdate, firstUpdated, updated, updateComplete), y finalmente dos técnicas de composición para reutilizar toda esa lógica entre varios componentes sin duplicarla: los controladores reactivos, recomendados quando el comportamiento tiene estado propio y conviene mantenerlo desacoplado, y los mixins, adecuados cuando el comportamiento debe integrarse de forma natural en la propia clase y su API pública, a costa de asumir el riesgo de colisiones de nombres y de un orden de composición que puede volverse difícil de razonar con varios mixins combinados.

Con el ciclo de vida ya dominado en todas sus formas, el curso vuelve la mirada hacia un terreno distinto: el de las plantillas. El módulo 7, "Directivas y Funcionalidades Avanzadas de Plantillas", presentará un conjunto de herramientas —directivas como classMap, styleMap o until, entre otras— que, en más de un caso, permiten simplificar patrones que este mismo curso ya ha resuelto a mano hasta ahora con código JavaScript explícito, exactamente el tipo de simplificación que conviene apreciar mejor una vez se entiende, como ya ocurre a partir de este módulo, qué ocurre realmente por debajo cuando una plantilla se vuelve a renderizar.

Errores Comunes y Consejos

  • Olvidar propagar Base.properties al declarar static properties dentro de un mixin (o en la clase final que lo usa): como se ha visto en los apartados 2 y 4, sin ...Base.properties cualquier propiedad reactiva declarada en un nivel distinto de la cadena de mixins deja de registrarse como reactiva, produciendo errores silenciosos donde una propiedad "no reacciona" sin ningún mensaje de error visible.
  • Encadenar demasiados mixins sobre un mismo componente: como se explica en el apartado 6, cada mixin adicional incrementa la dificultad de razonar sobre el orden de ejecución y sobre posibles colisiones; si un componente empieza a necesitar tres o cuatro mixins combinados, suele ser una señal de que al menos parte de esa lógica encajaría mejor como controladores reactivos independientes.
  • Usar un mixin para lógica con estado que no necesita integrarse en la API pública del componente: como se ha explicado en el apartado 5, si el comportamiento (como el temporizador de la lección anterior) puede vivir perfectamente desacoplado, sin que el resto del componente necesite tratarlo como una propiedad o método propios, un controlador reactivo evita por completo los riesgos de colisión de nombres y de orden de composición.
  • No documentar qué espera un mixin de su clase base: si ConEstadoCarga asumiera, por ejemplo, la existencia de un método render() con una forma concreta (más allá de recibir su resultado como argumento de conIndicadorDeCarga), cualquier componente que lo use necesitaría conocer ese contrato implícito; cuanto más claro y mínimo sea lo que un mixin exige de su base, más fácil será reutilizarlo con seguridad en componentes futuros.

Ejercicios

  1. Escribe un segundo mixin, ConContadorDeErrores, que añada una propiedad de estado ultimoError (inicializada a null) y un método registrarError(mensaje) que la actualice. Aplícalo junto con ConEstadoCarga sobre TaskBoard (class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement))), y comprueba que ambas propiedades (cargando y ultimoError) conviven sin problemas, dado que no comparten ningún nombre.
  2. Explica, basándote en el apartado 7, qué pasaría si ConContadorDeErrores del ejercicio anterior declarase también una propiedad llamada cargando (por ejemplo, para indicar si se está reintentando una operación tras un error), y en qué orden de aplicación de los dos mixins prevalecería cada valor.
  3. Retoma el ContadorTiempoRestanteController de la lección anterior y explica, con tus propias palabras, por qué no tendría sentido reescribirlo como un mixin (ConContadorDeTiempoRestante = (Base) => class extends Base {...}) aplicado directamente sobre TaskCard, apoyándote en el criterio del apartado 5.

Soluciones

// 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 },
  };
}

Como cargando y ultimoError son nombres distintos, ambos mixins añaden sus propiedades sin ningún conflicto, y TaskBoard termina con acceso a this.cargando, this.conIndicadorDeCarga(...), this.ultimoError y this.registrarError(...), todos disponibles simultáneamente.

  1. Si ambos mixins declarasen una propiedad cargando, el mixin aplicado en último lugar (el más externo en el anidamiento, es decir, el primero que aparece leyendo la expresión de izquierda a derecha) sería quien defina la versión final de static properties.cargando y, en su constructor, la última asignación de this.cargando = ... en ejecutarse (dado que cada mixin llama a super(...args) antes de asignar su propio valor por defecto, el mixin más externo se ejecuta después de la cadena de super, y su asignación es la que queda vigente al terminar la construcción). En class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)), sería ConContadorDeErrores quien prevalezca, y el significado original de cargando aportado por ConEstadoCarga quedaría silenciosamente sobrescrito, sin ningún aviso de error.

  2. ContadorTiempoRestanteController mantiene un estado interno (el identificador del intervalo, el valor actual de cercaDeVencer) que no necesita, en ningún momento, sentirse parte de la API pública de TaskCard; el resto del componente solo necesita leer this._contadorTiempo.cercaDeVencer desde render(), sin que cercaDeVencer tenga por qué ser una propiedad reactiva más de TaskCard en pie de igualdad con titulo o estado. Convertirlo en un mixin obligaría a fusionar ese estado directamente en TaskCard (como ocurre con cargando en ConEstadoCarga), incrementando el riesgo de colisión de nombres si, en el futuro, TaskCard u otro mixin aplicado sobre ella necesitara también una propiedad llamada cercaDeVencer o similar; además, el mixin heredaría también el problema de orden de composición del apartado 6 en cuanto se combinara con cualquier otro mixin futuro, algo que un controlador reactivo, aislado en su propio objeto, evita por completo.

Conclusión

Este módulo ha explicado con detalle el ciclo de vida completo de un componente Lit: los callbacks heredados de Custom Elements, los hooks propios del ciclo de actualización de Lit, y dos técnicas de composición —controladores reactivos y mixins— para reutilizar comportamiento entre componentes sin duplicar código. ConEstadoCarga, el mixin de esta última lección, ha mostrado el caso en el que fusionar comportamiento directamente en la clase de un componente tiene sentido, y la comparación con ContadorTiempoRestanteController ha dejado un criterio claro para decidir, en cualquier situación futura de TaskFlow, entre una técnica y la otra.

Con el ciclo de vida ya dominado, toca ver funcionalidades avanzadas de plantillas (directivas) que simplifican patrones ya vistos.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados