Les dues lliçons anteriors han deixat <task-card> amb una peça de lògica autocontinguda però atrapada dins de la seva pròpia classe: un temporitzador que arrenca a connectedCallback, s'atura a disconnectedCallback, i un càlcul (_calcularSiCercaDeVencer) que tots dos comparteixen. Funciona perfectament per a <task-card>, però si demà <task-board> necessités mostrar, per exemple, un resum amb el nombre de tasques a prop de la seva data límit, aquesta mateixa lògica hauria de copiar-se i enganxar-se en una segona classe, amb el risc que les dues còpies acabin divergint amb el temps. Aquesta lliçó presenta l'eina que Lit ofereix específicament per a aquest problema: els controladors reactius, objectes reutilitzables capaços d'enganxar-se al cicle de vida de qualsevol component sense necessitat d'heretar-hi.
Contingut
- El problema: lògica amb cicle de vida propi, atrapada en una classe
- Què és un
ReactiveController - La interfície d'un controlador:
hostConnected,hostDisconnected,hostUpdate,hostUpdated addController: registrant un controlador al seu host- Extraient el temporitzador de
<task-card>a un controlador reutilitzable - Usant el controlador des de
<task-card> - Per què un controlador i no simplement una funció auxiliar
- Tancament: controladors en front de mixins
- El problema: lògica amb cicle de vida propi, atrapada en una classe
La lògica afegida a la lliçó 06-01 té una característica particular que la distingeix d'un simple mètode auxiliar: necessita enganxar-se al cicle de vida del component que la usa. No n'hi ha prou d'extreure _calcularSiCercaDeVencer a una funció independent (això ja seria trivial, sense necessitat de cap concepte nou); el problema de fons és que el temporitzador en si —quan arrenca, quan s'atura— depèn de quan <task-card> es connecta i es desconnecta del DOM, i aquesta dependència del cicle de vida és, precisament, el que fa que copiar i enganxar aquesta lògica en un segon component sigui tan temptador com propens a errors: qualsevol correcció futura (per exemple, ajustar el marge de "a prop de vèncer" de 24 hores a 48) hauria d'aplicar-se en totes les còpies per separat.
- Què és un
ReactiveController
ReactiveControllerUn ReactiveController és, en la seva forma més simple, un objecte corrent de JavaScript (no necessita heretar de cap classe de Lit) que implementa una o diverses d'un petit conjunt de funcions amb noms reservats, i que es registra explícitament sobre un component Lit (anomenat, en aquesta relació, el seu host) per rebre avisos en els mateixos moments clau del cicle de vida que ja s'han estudiat a les lliçons anteriors d'aquest mòdul.
La idea central, i la que resol el problema de l'apartat 1, és que un mateix controlador —una mateixa classe de JavaScript— es pot registrar sobre tants hosts diferents com calgui, cada un amb la seva pròpia instància del controlador, sense que cap necessiti heretar d'una classe base comuna ni duplicar codi: el controlador encapsula tant l'estat (per exemple, l'identificador de l'interval actiu) com el comportament (arrencar-lo, aturar-lo, calcular el resultat), i el component que l'usa es limita a mantenir una referència al controlador i a llegir, des del seu render(), la dada que el controlador exposa.
- La interfície d'un controlador:
hostConnected, hostDisconnected, hostUpdate, hostUpdated
hostConnected, hostDisconnected, hostUpdate, hostUpdatedUn controlador pot implementar qualsevol d'aquests mètodes, tots opcionals, i Lit els crida automàticament en el moment corresponent del cicle de vida del seu host:
| Mètode del controlador | Es crida quan el host executa... | Equivalent conceptual vist en aquest mòdul |
|---|---|---|
hostConnected() |
connectedCallback() |
Lliçó 06-01: arrencar un efecte actiu |
hostDisconnected() |
disconnectedCallback() |
Lliçó 06-01: netejar aquest efecte |
hostUpdate() |
Abans de render(), a cada actualització |
Lliçó 06-02: equivalent a willUpdate |
hostUpdated() |
Després de cada render(), amb el DOM ja actualitzat |
Lliçó 06-02: equivalent a updated |
Aquesta correspondència no és casual: els controladors reactius estan dissenyats deliberadament com una via alternativa per enganxar-se als mateixos punts exactes del cicle de vida que ja s'han estudiat com a mètodes de la pròpia classe del component, amb la diferència que, en lloc de viure mesclats dins de la classe del component, viuen en un objecte a part, reutilitzable de manera independent.
addController: registrant un controlador al seu host
addController: registrant un controlador al seu hostPerquè un controlador comenci a rebre aquests avisos, el seu host —qualsevol LitElement, sense necessitat de cap configuració addicional— l'ha de registrar explícitament amb this.addController(controlador), normalment dins del constructor del component:
class TaskCard extends LitElement {
constructor() {
super();
this._miControlador = new AlgunControlador(this);
}
}És habitual, com en aquest exemple, que el propi constructor del controlador rebi el host com a argument i cridi ell mateix host.addController(this) internament, de manera que instanciar el controlador i registrar-lo sigui un únic pas, tal com es veurà a l'apartat següent. A partir d'aquest registre, Lit s'encarrega d'invocar els mètodes de la taula anterior en el moment adequat, exactament igual que si fossin mètodes de la pròpia classe del component.
- Extraient el temporitzador de
<task-card> a un controlador reutilitzable
<task-card> a un controlador reutilitzableAmb la interfície ja clara, la lògica de la lliçó 06-01 es trasllada, gairebé sense canvis conceptuals, a una classe independent:
// src/controllers/contador-tiempo-restante-controller.js
export class ContadorTiempoRestanteController {
constructor(host, { margenMs = 24 * 60 * 60 * 1000, intervaloMs = 60000 } = {}) {
this.host = host;
this.margenMs = margenMs;
this.intervaloMs = intervaloMs;
this.cercaDeVencer = false;
host.addController(this);
}
hostConnected() {
this._comprobar();
this._idIntervalo = setInterval(() => this._comprobar(), this.intervaloMs);
}
hostDisconnected() {
clearInterval(this._idIntervalo);
}
_comprobar() {
const fechaLimite = this.host.fechaLimite;
const nuevoValor = this._calcularSiCercaDeVencer(fechaLimite);
if (nuevoValor !== this.cercaDeVencer) {
this.cercaDeVencer = nuevoValor;
this.host.requestUpdate();
}
}
_calcularSiCercaDeVencer(fechaLimite) {
if (!fechaLimite) {
return false;
}
const msRestantes = fechaLimite.getTime() - Date.now();
return msRestantes > 0 && msRestantes <= this.margenMs;
}
}Diversos detalls distingeixen aquest codi de l'original de la lliçó 06-01, i tots responen a la mateixa necessitat: fer-lo reutilitzable per qualsevol host, no només per <task-card>.
- El constructor guarda una referència al
hostrebut com a paràmetre, i crida ell mateixhost.addController(this): qui usi aquest controlador no necessita recordar-se de cridaraddControllerper separat, n'hi ha prou d'instanciar-lo passant-lithis(el propi component) com a argument. - El marge de "a prop de vèncer" (
margenMs) i la freqüència de comprovació (intervaloMs) s'han convertit en paràmetres configurables, amb valors per defecte raonables, en lloc d'estar fixats directament al codi com a la lliçó 06-01: això permet que un futur segon host usi, per exemple, un marge de 48 hores sense haver de copiar i modificar el controlador. - El controlador llegeix
this.host.fechaLimitedirectament, assumint que qualsevol host que l'usi exposarà una propietat amb aquest nom. Aquesta és l'única suposició que el controlador fa sobre el seu host, i convé documentar-la amb claredat: un controlador reutilitzable sempre necessita algun contracte mínim sobre allò que el seu host li ha d'oferir. - Fonamental: en lloc d'assignar directament una propietat reactiva del host (
this.host.cercaDeVencer = ..., com feia<task-card>a la lliçó 06-01), el controlador guarda el seu propi resultat en un camp propi (this.cercaDeVencer, en el propi controlador, no en el host) i crida explícitamentthis.host.requestUpdate()per demanar a Lit que torni a executarrender(). El controlador no necessita declarar cap propietat reactiva de Lit per al seu propi estat intern; li n'hi ha prou ambrequestUpdate(), exactament el mateix mecanisme de baix nivell estudiat al mòdul 2, per provocar una actualització cada vegada que el seu resultat canviï.
- Usant el controlador des de
<task-card>
<task-card>Amb el controlador ja extret, <task-card> es simplifica notablement: desapareixen connectedCallback, disconnectedCallback i _calcularSiCercaDeVencer de la lliçó 06-01, substituïts per una única instància del controlador.
// src/components/task-card.js
import { ContadorTiempoRestanteController } from '../controllers/contador-tiempo-restante-controller.js';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
expandida: { state: true },
fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
};
constructor() {
super();
this.titulo = '';
this.estado = 'pendiente';
this.prioridad = 1;
this.urgente = false;
this.expandida = false;
this.fechaLimite = null;
this._contadorTiempo = new ContadorTiempoRestanteController(this);
}
render() {
return html`
<article @click="${this.alternarExpandida}">
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
${this.renderSelectorEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
${this._contadorTiempo.cercaDeVencer ? html`<p class="aviso">⏰ Está a punto de vencer</p>` : ''}
${this.expandida
? html`<div class="detalle"><p>Estado interno: la tarjeta está expandida.</p></div>`
: ''}
</article>
`;
}
}Val la pena notar que cercaDeVencer ja no existeix com a propietat d'estat de <task-card> (ha desaparegut de static properties): ara viu exclusivament dins del controlador, i render() la llegeix directament com this._contadorTiempo.cercaDeVencer. <task-card> ni tan sols necessita saber que, internament, aquest valor s'actualitza mitjançant un setInterval; només necessita saber que el controlador exposa una propietat cercaDeVencer llegible en qualsevol moment, i que Lit tornarà a executar render() automàticament cada vegada que aquest valor canviï, gràcies al requestUpdate() que el propi controlador invoca.
Si més endavant <task-board> necessités mostrar quantes tasques estan a prop de vèncer, podria instanciar el seu propi ContadorTiempoRestanteController per a cada tasca de this.tareas (o, més simple, reutilitzar el mateix càlcul agregant-lo des de diverses instàncies ja creades a cada <task-card>), sense duplicar ni una sola línia de la lògica del temporitzador: el controlador ja està escrit, provat i llest per usar-se en qualsevol host que exposi una propietat fechaLimite.
- Per què un controlador i no simplement una funció auxiliar
Cal preguntar-se per què fa falta un objecte amb aquesta interfície concreta, en lloc de limitar-se a extreure _calcularSiCercaDeVencer a una funció solta i importar-la on calgui. La resposta rau en allò que una funció solta no pot resoldre per si sola: el cicle de vida i l'estat que persisteix entre crides. Una funció auxiliar sense estat propi serviria perfectament per al càlcul puntual (_calcularSiCercaDeVencer(fechaLimite) ja ho era, de fet, des de la lliçó 06-01), però no pot, per si mateixa, arrencar un temporitzador quan el host es connecta, aturar-lo quan es desconnecta, ni recordar l'identificador d'aquest temporitzador entre una crida i la següent. Això exigeix un objecte amb estat propi (l'_idIntervalo, el cercaDeVencer actual) i amb enganxaments al cicle de vida del host —exactament el que la interfície de ReactiveController proporciona de manera estandarditzada, en lloc que cada component reinventi la seva pròpia manera de coordinar temporitzadors amb connectedCallback i disconnectedCallback.
- Tancament: controladors en front de mixins
Els controladors reactius són, en la documentació i en la pràctica habitual de l'ecosistema de Lit, l'alternativa recomanada en front d'una altra tècnica més antiga i més general de JavaScript per compondre comportament reutilitzable entre classes: els mixins. Totes dues eines persegueixen el mateix objectiu general (reutilitzar lògica entre diversos components sense copiar i enganxar codi), però ho fan de formes prou diferents com perquè valgui la pena dedicar-los-hi una comparació completa abans de decidir quina usar en cada cas. Aquesta comparació, junt amb el patró dels mixins en si, és el contingut de la lliçó següent.
Errors Comuns i Consells
- Assignar directament una propietat reactiva del host des del controlador: com s'ha assenyalat a l'apartat 5, un controlador no necessita declarar propietats reactives pròpies en el host; li n'hi ha prou amb mantenir el seu propi estat intern i cridar
this.host.requestUpdate()quan aquest estat canviï. Intentar ferthis.host.cercaDeVencer = ...obligaria a més que el host declarés aquesta propietat en el seu propistatic properties, acoblant la implementació interna del controlador a la declaració de propietats de cada host concret. - Oblidar
host.addController(this)dins del propi constructor del controlador: sense aquest registre, Lit mai cridahostConnectednihostDisconnected, i el controlador es comporta com un objecte corrent sense cap connexió real al cicle de vida del seu host, malgrat que el seu codi pugui semblar correcte a simple vista. - Fer que el controlador depengui de propietats molt específiques d'un únic component: com més genèric sigui el contracte que el controlador exigeix del seu host (en aquest exemple, una única propietat
fechaLimite, amb paràmetres configurables per a la resta), més fàcil serà reutilitzar-lo en components futurs que ni tan sols existeixen encara; un controlador que assumís, per exemple, l'existència d'un mètode molt concret de<task-card>perdria bona part del seu valor com a peça reutilitzable. - No netejar recursos a
hostDisconnected: exactament el mateix risc assenyalat a la lliçó 06-01 sobredisconnectedCallbacks'aplica aquí; un controlador que arrenca un temporitzador o una subscripció ahostConnectedi no l'allibera ahostDisconnectedprovoca la mateixa fuga de memòria, ara potencialment multiplicada per cada host que l'usi.
Exercicis
- Afegeix a
ContadorTiempoRestanteControllerun segon paràmetre de configuració,onCambio, una funció de callback opcional que el controlador invoqui (a més de cridarrequestUpdate()) cada vegada quecercaDeVencercanviï de valor, passant-li el nou valor com a argument. Modifica la instanciació a<task-card>per aprofitar aquest callback i mostrar unconsole.logquan la targeta entri o surti de l'estat "a prop de vèncer". - Explica, basant-te en l'apartat 3, quina diferència hi ha entre implementar
hostUpdate()en un controlador i sobreescriurewillUpdate()directament en la classe del component, quant al lloc on viu físicament el codi. - Un company d'equip, en veure el controlador de l'apartat 5, proposa que en lloc de
this.host.requestUpdate()seria més simple fer que el propi host exposés una propietat reactivacercaDeVenceri que el controlador l'assignés directament. Argumenta, basant-te en l'apartat 7 i en el propi codi de l'apartat 6, per què l'enfocament actual (la dada viu en el controlador, no en el host) fa que<task-card>necessiti canviar menys codi si el controlador se substituís en el futur per una implementació diferent.
Solucions
export class ContadorTiempoRestanteController {
constructor(host, { margenMs = 24 * 60 * 60 * 1000, intervaloMs = 60000, onCambio } = {}) {
this.host = host;
this.margenMs = margenMs;
this.intervaloMs = intervaloMs;
this.onCambio = onCambio;
this.cercaDeVencer = false;
host.addController(this);
}
_comprobar() {
const nuevoValor = this._calcularSiCercaDeVencer(this.host.fechaLimite);
if (nuevoValor !== this.cercaDeVencer) {
this.cercaDeVencer = nuevoValor;
this.host.requestUpdate();
this.onCambio?.(nuevoValor);
}
}
// hostConnected(), hostDisconnected() i _calcularSiCercaDeVencer() sense canvis.
}this._contadorTiempo = new ContadorTiempoRestanteController(this, {
onCambio: (valor) => console.log(`cercaDeVencer ahora es ${valor}`),
});- Implementar
hostUpdate()en un controlador manté aquest codi físicament dins de la classe del controlador, separat per complet de la classe del component; sobreescriurewillUpdate()directament en el component mescla aquest mateix tipus de lògica (alguna cosa que s'ha d'executar abans derender()) dins del propi cos de la classeTaskCard. Tots dos s'executen en el mateix moment exacte del cicle, però només la versió amb controlador permet reutilitzar aquesta lògica en un segon component sense copiar codi: la versió ambwillUpdate()directament en la classe queda lligada a aquesta classe concreta. - Amb el disseny actual,
<task-card>no sap absolutament res sobre com el controlador calculacercaDeVencerinternament: només coneix quethis._contadorTiempoexposa una propietatcercaDeVencerllegible arender(). Si en el futur se substituísContadorTiempoRestanteControllerper una implementació diferent (per exemple, una que en lloc d'unsetIntervalusés alguna API del navegador més eficient per a temporitzadors en segon pla),<task-card>no hauria de canviar ni una línia, sempre que la nova implementació continués exposant la mateixa propietatcercaDeVencer. Si, en canvi, el controlador assignés directamentthis.host.cercaDeVencer,<task-card>hauria de continuar declarant aquesta propietat en el seu propistatic propertiesúnicament per donar cabuda a un detall d'implementació intern del controlador, mesclant la superfície pública de<task-card>amb el mecanisme intern d'una peça que, en teoria, hauria de ser substituïble sense fricció.
Conclusió
Aquesta lliçó ha presentat els controladors reactius com l'alternativa recomanada de Lit per encapsular lògica reutilitzable amb estat propi i necessitats de cicle de vida: un objecte corrent de JavaScript, registrat amb addController, capaç d'enganxar-se a hostConnected, hostDisconnected, hostUpdate i hostUpdated exactament en els mateixos punts que ja es coneixien com a mètodes de la pròpia classe d'un component. El temporitzador de <task-card>, fins ara atrapat dins de la seva pròpia classe, viu ara a ContadorTiempoRestanteController, llest per reutilitzar-se en qualsevol altre component de TaskFlow que necessiti el mateix tipus d'avís per proximitat de data.
Queda per resoldre, però, quan convé un controlador reactiu i quan convé l'alternativa més clàssica de JavaScript per a aquest mateix problema general: els mixins. La lliçó següent explica el patró dels mixins aplicat a Lit, les seves limitacions més freqüents, i el criteri concret per triar entre una tècnica i l'altra segons el tipus de comportament que es vulgui compartir.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
