La lliçó anterior ha resolt, amb controladors reactius, la reutilització de lògica amb estat propi i cicle de vida, com el temporitzador de proximitat de data de <task-card>. Però no tot comportament reutilitzable encaixa bé en aquest motlle: de vegades allò que es vol compartir entre diversos components no és un objecte independent amb el seu propi estat intern, sinó propietats i mètodes que haurien de passar a formar part de la pròpia classe i de la pròpia API pública del component, com si s'haguessin escrit directament en ella. Per a aquest segon cas, JavaScript ofereix una tècnica més general i anterior a Lit: els mixins. Aquesta lliçó explica el patró, l'aplica a un petit exemple de TaskFlow, i tanca amb el criteri per decidir, en cada situació futura, entre un mixin i un controlador reactiu.

Contingut

  1. Què és un mixin en JavaScript pur
  2. El patró típic d'un mixin a Lit
  3. Aplicant un mixin: ConEstadoCarga
  4. Usant el mixin en un component de TaskFlow
  5. Mixin en front de controlador reactiu: el criteri de decisió
  6. Limitacions dels mixins: ordre de composició
  7. Limitacions dels mixins: col·lisió de noms
  8. Tancament del mòdul 6

  1. Què és un mixin en JavaScript pur

Un mixin, en JavaScript, no és una paraula clau del llenguatge ni una característica especial: és, senzillament, una funció que rep una classe com a argument i retorna una nova classe que estén la rebuda, afegint-li propietats o mètodes addicionals. No hi ha res específic de Lit en aquesta idea; és una tècnica general de composició de classes que existeix en JavaScript des que les classes mateixes (class) formen part del llenguatge, aprofitant que extends pot rebre qualsevol expressió que avaluï a una classe, no només un nom de classe escrit literalment.

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 estén el resultat de cridar MiMixin(ClaseOriginal), que és, en si mateix, una classe nova (anònima, definida amb class extends ClaseBase { ... } dins del cos de la funció) que hereta de ClaseOriginal i li afegeix metodoNuevo. El resultat final té accés tant a allò que ja existia a ClaseOriginal com a allò que aporta el mixin, exactament com si s'hagués escrit una única classe amb tot junt, però mantenint MiMixin com una peça separada i reutilitzable amb qualsevol altra classe base.

  1. El patró típic d'un mixin a Lit

Aplicat a components de Lit, el patró és idèntic, amb la particularitat que la "classe base" rebuda pel mixin sol ser, en darrer terme, LitElement (o el resultat d'aplicar ja un altre 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 detalls d'aquest patró mereixen atenció abans d'aplicar-lo a un exemple real. Primer, static properties = { ...Base.properties, propiedadNueva: {...} }: com static properties és un objecte normal de JavaScript, cal combinar explícitament, amb l'operador de propagació, les propietats ja declarades per la classe base amb les noves que aporta el mixin; oblidar el ...Base.properties faria que qualsevol propietat reactiva declarada més avall en la cadena d'herència (per exemple, directament a MiComponente, si estengués static properties al seu torn) deixés de funcionar correctament, perquè MiMixin l'hauria sobreescrit amb un objecte que no la inclou. Segon, el constructor(...args) { super(...args); ... }: un mixin ha de reenviar fidelment qualsevol argument rebut a super(...args), perquè no pot saber d'antuvi quins arguments espera la classe base sobre la qual s'aplicarà (en el cas de LitElement, el propi constructor no sol rebre arguments, però el mixin no hauria d'assumir-ho si aspira a ser reutilitzable amb qualsevol classe base).

  1. Aplicant un mixin: ConEstadoCarga

TaskFlow no té, per ara, cap operació que impliqui una espera visible (com una petició de xarxa; això arribarà al mòdul 8), però serveix com a exemple clar i autocontingut d'un comportament que diversos components de l'aplicació podrien necessitar compartir en un futur pròxim: una propietat cargando i una forma reutilitzable d'embolcallar una plantilla amb un indicador visual mentre aquesta propietat estigui 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 afegeix dues coses a qualsevol classe sobre la qual s'apliqui: la propietat d'estat cargando (inicialitzada a false), i el mètode conIndicadorDeCarga(plantilla), que rep la plantilla "normal" del component i retorna, en el seu lloc, un avís de càrrega si cargando és true. Val la pena notar que, a diferència del controlador reactiu de la lliçó anterior, aquí no hi ha cap objecte separat: cargando passa a ser una propietat reactiva més de la pròpia classe final, tan accessible com titulo o estado a <task-card>, i conIndicadorDeCarga passa a ser un mètode més d'aquesta mateixa classe, invocable com this.conIndicadorDeCarga(...) des de dins de render().

  1. Usant el mixin en un component de TaskFlow

Aplicar el mixin a un component és tan directe com embolcallar LitElement en la crida a la funció:

// 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 estén ConEstadoCarga(LitElement) en lloc de LitElement directament, i a partir d'aquest moment té accés, com si fossin seus des de sempre, tant a la propietat cargando com al mètode conIndicadorDeCarga. El propi render() de TaskBoard embolcalla la seva plantilla habitual en una crida a this.conIndicadorDeCarga(...): mentre this.cargando sigui false (el seu valor per defecte), el comportament és idèntic al d'abans d'aplicar el mixin; en quant algun codi activi this.cargando = true (per exemple, en iniciar una operació que triga un temps a completar-se), render() mostrarà l'avís de càrrega en el seu lloc, sense que TaskBoard hagi hagut d'escriure aquesta lògica condicional per si mateixa.

Val la pena notar la construcció, una mica repetitiva, de static properties = { ...ConEstadoCarga(LitElement).properties, tareas: {...} }: com s'ha explicat a l'apartat 2, cada nivell de la cadena que afegeixi les seves pròpies propietats reactives s'ha de recordar de propagar també les heretades del nivell anterior, i això inclou la pròpia classe final que usa el mixin, no només el mixin en si. És un detall de manteniment a tenir present cada vegada que es combina un mixin amb propietats pròpies del component final.

  1. Mixin en front de controlador reactiu: el criteri de decisió

Amb les dues tècniques ja vistes en aquest mòdul, convé fixar un criteri clar per triar entre elles, en lloc d'aplicar-les de forma intercanviable sense raonar-ne el perquè:

Criteri Mixin Controlador reactiu
On viu l'estat afegit? Directament a la instància del component (this.cargando) En un objecte propi, separat del host (this._contadorTiempo.cercaDeVencer)
S'integra a l'API pública del component? Sí: les seves propietats i mètodes passen a ser indistingibles dels propis del component No necessàriament: el host decideix què exposar, si és que exposa alguna cosa
Com s'activa? Embolcallant la classe amb extends MiMixin(Base) Instanciant-lo dins del constructor amb new Controlador(this)
Millor cas d'ús Comportament que hauria de sentir-se part de la pròpia classe (un mètode d'utilitat, una propietat que la resta del component usa amb naturalitat) Lògica amb estat propi i necessitats de cicle de vida, pensada per romandre desacoblada del host
Exemple d'aquest curs ConEstadoCarga: cargando i conIndicadorDeCarga se senten part natural de TaskBoard ContadorTiempoRestanteController: el temporitzador no necessita fondre's amb l'API pública de TaskCard

El criteri de fons, resumit en una frase, és aquest: un mixin és adequat quan el comportament hauria d'integrar-se en la pròpia classe i la seva API pública, com si s'hagués escrit allà directament; un controlador reactiu és preferible quan la lògica té el seu propi estat intern i convé mantenir-la desacoblada, com una peça que el host usa però de la qual no necessita heretar ni exposar directament els seus detalls. La documentació oficial de Lit es decanta, de fet, per recomanar controladors reactius com a primera opció en front dels mixins en la majoria dels casos de lògica amb estat, precisament pels problemes que s'expliquen en els dos apartats següents.

  1. Limitacions dels mixins: ordre de composició

Quan s'aplica un únic mixin, com a l'exemple de ConEstadoCarga, l'ordre no genera cap ambigüitat. El problema apareix en quant es combinen diversos mixins sobre la mateixa classe base:

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

Aquí, MixinB s'aplica primer sobre LitElement, i MixinA s'aplica després sobre el resultat de MixinB(LitElement). Si tots dos mixins sobreescriuen, per exemple, el mateix mètode del cicle de vida (connectedCallback, o qualsevol altre), l'ordre d'anidament determina quina de les dues versions "veu" primer la crida i quina depèn de que l'altra invoqui super correctament per no perdre el seu propi comportament. Amb dos mixins, aquest raonament ja exigeix certa cura; amb tres o més, aplicats en ordres diferents en diferents components d'una mateixa aplicació, el resultat pot tornar-se difícil de predir sense llegir amb atenció el codi de cada mixin implicat, cosa que rares vegades passa amb un únic controlador reactiu (o amb diversos, registrats de manera independent mitjançant addController, sense cap relació d'ordre d'herència entre ells).

  1. Limitacions dels mixins: col·lisió de noms

El segon problema freqüent dels mixins és la col·lisió de noms: si dos mixins diferents, aplicats sobre la mateixa classe, declaren una propietat o un mètode amb el mateix nom (o si un mixin usa, sense adonar-se'n, un nom que la pròpia classe final ja usava pel seu compte), un dels dos sobreescriu silenciosament l'altre, sense que JavaScript emeti cap avís ni error. Per exemple, si un segon mixin de TaskFlow, pensat per gestionar errors, també declarés una propietat anomenada cargando (potser amb un significat lleugerament diferent), i es combinés amb ConEstadoCarga sobre el mateix component, un dels dos valors de cargando prevaldria sobre l'altre segons l'ordre d'aplicació, i el resultat seria difícil de depurar sense conèixer el codi intern de tots dos mixins.

Aquest risc és, precisament, un dels motius principals pels quals un controlador reactiu sol ser més segur: com que el seu estat viu en un objecte propi (this._contadorTiempo, no directament this), dos controladors diferents mai poden col·lisionar entre si ni amb les propietats del propi host, encara que usin internament noms de camp idèntics, perquè cada un viu en el seu propi espai de noms, aïllat de la resta.

  1. Tancament del mòdul 6

Amb aquesta lliçó es completa el mòdul 6. El recorregut ha anat de menys a més control sobre el cicle de vida d'un component: primer els callbacks heretats de Custom Elements (connectedCallback, disconnectedCallback), després els hooks propis del cicle d'actualització de Lit (willUpdate, firstUpdated, updated, updateComplete), i finalment dues tècniques de composició per reutilitzar tota aquesta lògica entre diversos components sense duplicar-la: els controladors reactius, recomanats quan el comportament té estat propi i convé mantenir-lo desacoblat, i els mixins, adequats quan el comportament ha d'integrar-se de forma natural en la pròpia classe i la seva API pública, a costa d'assumir el risc de col·lisions de noms i d'un ordre de composició que pot tornar-se difícil de raonar amb diversos mixins combinats.

Amb el cicle de vida ja dominat en totes les seves formes, el curs torna la mirada cap a un terreny diferent: el de les plantilles. El mòdul 7, "Directives i Funcionalitats Avançades de Plantilles", presentarà un conjunt d'eines —directives com classMap, styleMap o until, entre altres— que, en més d'un cas, permeten simplificar patrons que aquest mateix curs ja ha resolt a mà fins ara amb codi JavaScript explícit, exactament el tipus de simplificació que convé apreciar millor una vegada s'entén, com ja passa a partir d'aquest mòdul, què passa realment per sota quan una plantilla es torna a renderitzar.

Errors Comuns i Consells

  • Oblidar propagar Base.properties en declarar static properties dins d'un mixin (o en la classe final que l'usa): com s'ha vist als apartats 2 i 4, sense ...Base.properties qualsevol propietat reactiva declarada en un nivell diferent de la cadena de mixins deixa de registrar-se com a reactiva, produint errors silenciosos on una propietat "no reacciona" sense cap missatge d'error visible.
  • Encadenar massa mixins sobre un mateix component: com s'explica a l'apartat 6, cada mixin addicional incrementa la dificultat de raonar sobre l'ordre d'execució i sobre possibles col·lisions; si un component comença a necessitar tres o quatre mixins combinats, sol ser un senyal que almenys part d'aquesta lògica encaixaria millor com a controladors reactius independents.
  • Usar un mixin per a lògica amb estat que no necessita integrar-se en l'API pública del component: com s'ha explicat a l'apartat 5, si el comportament (com el temporitzador de la lliçó anterior) pot viure perfectament desacoblat, sense que la resta del component necessiti tractar-lo com una propietat o mètode propis, un controlador reactiu evita per complet els riscos de col·lisió de noms i d'ordre de composició.
  • No documentar què espera un mixin de la seva classe base: si ConEstadoCarga assumís, per exemple, l'existència d'un mètode render() amb una forma concreta (més enllà de rebre el seu resultat com a argument de conIndicadorDeCarga), qualsevol component que l'usi necessitaria conèixer aquest contracte implícit; com més clar i mínim sigui allò que un mixin exigeix de la seva base, més fàcil serà reutilitzar-lo amb seguretat en components futurs.

Exercicis

  1. Escriu un segon mixin, ConContadorDeErrores, que afegeixi una propietat d'estat ultimoError (inicialitzada a null) i un mètode registrarError(mensaje) que l'actualitzi. Aplica'l junt amb ConEstadoCarga sobre TaskBoard (class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement))), i comprova que ambdues propietats (cargando i ultimoError) convivencen sense problemes, atès que no comparteixen cap nom.
  2. Explica, basant-te en l'apartat 7, què passaria si ConContadorDeErrores de l'exercici anterior declarés també una propietat anomenada cargando (per exemple, per indicar si s'està reintentant una operació després d'un error), i en quin ordre d'aplicació dels dos mixins prevaldria cada valor.
  3. Reprèn el ContadorTiempoRestanteController de la lliçó anterior i explica, amb les teves pròpies paraules, per què no tindria sentit reescriure'l com un mixin (ConContadorDeTiempoRestante = (Base) => class extends Base {...}) aplicat directament sobre TaskCard, recolzant-te en el criteri de l'apartat 5.

Solucions

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

Com que cargando i ultimoError són noms diferents, tots dos mixins afegeixen les seves propietats sense cap conflicte, i TaskBoard acaba amb accés a this.cargando, this.conIndicadorDeCarga(...), this.ultimoError i this.registrarError(...), tots disponibles simultàniament.

  1. Si tots dos mixins declaressin una propietat cargando, el mixin aplicat en últim lloc (el més extern en l'anidament, és a dir, el primer que apareix llegint l'expressió d'esquerra a dreta) seria qui defineixi la versió final de static properties.cargando i, en el seu constructor, la darrera assignació de this.cargando = ... en executar-se (atès que cada mixin crida super(...args) abans d'assignar el seu propi valor per defecte, el mixin més extern s'executa després de la cadena de super, i la seva assignació és la que queda vigent en acabar la construcció). A class TaskBoard extends ConContadorDeErrores(ConEstadoCarga(LitElement)), seria ConContadorDeErrores qui prevaldria, i el significat original de cargando aportat per ConEstadoCarga quedaria silenciosament sobreescrit, sense cap avís d'error.

  2. ContadorTiempoRestanteController manté un estat intern (l'identificador de l'interval, el valor actual de cercaDeVencer) que no necessita, en cap moment, sentir-se part de l'API pública de TaskCard; la resta del component només necessita llegir this._contadorTiempo.cercaDeVencer des de render(), sense que cercaDeVencer hagi de ser una propietat reactiva més de TaskCard en peu d'igualtat amb titulo o estado. Convertir-lo en un mixin obligaria a fusionar aquest estat directament a TaskCard (com passa amb cargando a ConEstadoCarga), incrementant el risc de col·lisió de noms si, en el futur, TaskCard o un altre mixin aplicat sobre ella necessités també una propietat anomenada cercaDeVencer o similar; a més, el mixin heretaria també el problema d'ordre de composició de l'apartat 6 en quant es combinés amb qualsevol altre mixin futur, cosa que un controlador reactiu, aïllat en el seu propi objecte, evita per complet.

Conclusió

Aquest mòdul ha explicat amb detall el cicle de vida complet d'un component Lit: els callbacks heretats de Custom Elements, els hooks propis del cicle d'actualització de Lit, i dues tècniques de composició —controladors reactius i mixins— per reutilitzar comportament entre components sense duplicar codi. ConEstadoCarga, el mixin d'aquesta darrera lliçó, ha mostrat el cas en el qual fusionar comportament directament en la classe d'un component té sentit, i la comparació amb ContadorTiempoRestanteController ha deixat un criteri clar per decidir, en qualsevol situació futura de TaskFlow, entre una tècnica i l'altra.

Amb el cicle de vida ja dominat, toca veure funcionalitats avançades de plantilles (directives) que simplifiquen patrons ja vistos.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats