Les tres lliçons anteriors han cobert proves, accessibilitat i rendiment, cadascuna com un angle de qualitat transversal aplicat sobre TaskFlow tal com va quedar al final del mòdul 8. Aquesta darrera lliçó del mòdul tanca amb una síntesi diferent: en lloc d'un angle nou, recorre de principi a fi les decisions de disseny ja preses durant el curs, les agrupa en parelles de patró recomanat enfront d'antipatró, i explica per què cada antipatró, encara que a vegades "funcioni" a curt termini, acaba costant més d'allò que estalvia. És, en certa manera, un repàs del curs complet vist des del revers: no què fa cada peça de Lit, sinó quin error concret s'evita en utilitzar-la bé.
Contingut
- Per què convé una lliçó de síntesi abans del projecte final
- Taula general: patrons enfront d'antipatrons del curs
- Immutabilitat enfront de mutar arrays i objectes al lloc
composed: trueenfront d'esdeveniments personalitzats que no surten del Shadow DOM- Plantilles declaratives enfront de lògica de negoci dins de
render() - Components descompostos enfront de components que acumulen responsabilitats
- Neteja de recursos enfront de fuites a
disconnectedCallbacki controladors - Un criteri final: preguntes que detecten un antipatró abans d'escriure'l
- Cap al projecte final: repassar TaskFlow de principi a fi
- Per què convé una lliçó de síntesi abans del projecte final
Cada antipatró d'aquesta lliçó ja va aparèixer, en algun moment del curs, assenyalat com a "error comú" al final d'una lliçó concreta, en el context estret de la tècnica que s'estava explicant en aquell moment. Allò que aquesta forma de presentar-los no podia mostrar és el patró que es repeteix entre mòduls diferents: que oblidar composed: true (mòdul 5) i no netejar un setInterval a disconnectedCallback (mòdul 6) són, en el fons, la mateixa classe d'error —oblidar la meitat d'una operació que exigeix simetria—, encara que apareguin en dues lliçons separades per diversos mòduls. Veure'ls junts, ara que ja es coneix el curs complet, ajuda a reconèixer-los amb més facilitat en codi nou, inclòs el que s'escriurà al mòdul 10.
- Taula general: patrons enfront d'antipatrons del curs
| # | Antipatró | Patró recomanat | On es va explicar |
|---|---|---|---|
| 1 | Mutar un array o un objecte al lloc dins d'una propietat reactiva | Substituir la propietat per una còpia nova (map, filter, {...obj}) |
Lliçó "Comunicació de Pare a Fill amb Propietats" (mòdul 5) |
| 2 | Despatxar un esdeveniment personalitzat sense composed: true des d'un component amb Shadow DOM |
bubbles: true i composed: true junts, perquè l'esdeveniment surti del shadow root |
Lliçó "Esdeveniments Personalitzats: Comunicació de Fill a Pare" (mòdul 5) |
| 3 | Ficar lògica de negoci o càlculs costosos directament dins de render() |
Derivar aquest càlcul a willUpdate, o extraure'l a un mètode/controlador a part |
Lliçons "Hooks Reactius" (mòdul 6) i "Rendiment i Optimització" (mòdul 9) |
| 4 | Un únic component que acumula massa responsabilitats sense descompondre's | Extreure subcomponents amb una responsabilitat clara, com <user-avatar> o <task-filter> |
Lliçons "Slots i Estilització de Contingut Distribuït" (mòdul 4) i "Context Compartit amb @lit/context" (mòdul 7) |
| 5 | No alliberar recursos (temporitzadors, subscripcions) a disconnectedCallback o a hostDisconnected |
Netejar en el mateix lloc simètric on es va arrencar el recurs | Lliçons "Callbacks del Cicle de Vida" i "Controladors Reactius" (mòdul 6) |
Les cinc files comparteixen una mateixa estructura de fons, encara que a primera vista semblin problemes diferents: cada antipatró "estalvia" una línia o un pas en el moment d'escriure'l, i cada patró recomanat inverteix aquest petit cost inicial a canvi d'un comportament predictible més endavant, quan el component creix, es reutilitza en un context diferent, o convivi amb altres que no controla directament.
- Immutabilitat enfront de mutar arrays i objectes al lloc
La lliçó "Comunicació de Pare a Fill amb Propietats" va explicar, amb la taula d'operacions de la lliçó 05-03, per què this.tareas.push(...) o this.tareas[0].estado = 'hecha' no disparen cap actualització visible a Lit: la comparació per defecte d'una propietat reactiva de tipus objecte o array és per referència, no per contingut, i una mutació al lloc mai canvia aquesta referència.
// Antipatró: muta l'array existent
gestionarTareaCambiada(idTarea, event) {
const tarea = this.tareas.find((t) => t.id === idTarea);
tarea.estado = event.detail.nuevoEstado;
this.requestUpdate(); // pedaç que amaga el problema real
}
// Patró recomanat: substitueix amb una còpia nova
gestionarTareaCambiada(idTarea, event) {
const nuevoEstado = event.detail.nuevoEstado;
this.tareas = this.tareas.map((tarea) =>
tarea.id === idTarea ? { ...tarea, estado: nuevoEstado } : tarea
);
}El this.requestUpdate() forçat a la primera versió no és una correcció, sinó un símptoma: "funciona" perquè obliga Lit a tornar a renderitzar sense importar si va detectar o no un canvi real, però no resol el problema de fons, i qualsevol altre codi que depengués de comparar l'array anterior amb l'actual (una eina de depuració amb historial, o una futura funcionalitat de "desfer") continuaria fallant, perquè els objectes mutats al lloc no deixen cap rastre de quin era el seu valor anterior. La versió immutable no necessita cap requestUpdate() manual perquè la pròpia assignació this.tareas = ... ja canvia la referència, que és justament el que el sistema de reactivitat de Lit necessita detectar per si mateix.
composed: true enfront d'esdeveniments personalitzats que no surten del Shadow DOM
composed: true enfront d'esdeveniments personalitzats que no surten del Shadow DOMLa lliçó "Esdeveniments Personalitzats: Comunicació de Fill a Pare" va assenyalar aquest error com el més freqüent en depurar comunicació entre components, i mereix repetir-se aquí junt amb la resta perquè comparteix la mateixa naturalesa que l'antipatró de l'apartat anterior: una operació que sembla completa, però li falta una segona meitat imprescindible.
// Antipatró: l'esdeveniment mai surt del shadow root de <task-card>
this.dispatchEvent(new CustomEvent('tarea-cambiada', { detail: { nuevoEstado } }));
// Patró recomanat: bubbles i composed junts
this.dispatchEvent(
new CustomEvent('tarea-cambiada', {
detail: { nuevoEstado },
bubbles: true,
composed: true,
})
);Sense composed: true, el codi de la primera versió no llança cap error ni cap advertència: l'esdeveniment es crea, es despatxa, i qualsevol console.log col·locat dins del propi mètode confirmaria, enganyosament, que "tot funciona". La fallada només es manifesta un pas més enllà, a <task-list>, que mai arriba a rebre l'esdeveniment perquè aquest queda atrapat dins del shadow root de <task-card>, exactament la classe de fallada silenciosa —sense missatge d'error, sense traça a la consola— que resulta més costosa de diagnosticar que un error explícit.
- Plantilles declaratives enfront de lògica de negoci dins de
render()
render()La lliçó "Rendiment i Optimització" (09-03) ja va explicar el cost de recalcular dins de render() alguna cosa que es podria derivar una sola vegada a willUpdate; aquest apartat recupera la mateixa idea des d'un angle diferent, el de la claredat del propi codi, no només el seu cost:
// Antipatró: decideix, calcula i formata tot dins de render()
render() {
const diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
const claseUrgencia = diasRestantes < 1 ? 'critico' : diasRestantes < 3 ? 'aviso' : 'normal';
return html`<p class="${claseUrgencia}">${diasRestantes} días restantes</p>`;
}
// Patró recomanat: render() només tradueix dades ja preparades a HTML
willUpdate(changedProperties) {
if (changedProperties.has('fechaLimite')) {
this._diasRestantes = Math.floor((this.fechaLimite - Date.now()) / 86400000);
this._claseUrgencia = this._diasRestantes < 1 ? 'critico' : this._diasRestantes < 3 ? 'aviso' : 'normal';
}
}
render() {
return html`<p class="${this._claseUrgencia}">${this._diasRestantes} días restantes</p>`;
}Més enllà del cost de recalcular a cada renderització (ja cobert a la lliçó anterior), la primera versió mescla dues responsabilitats diferents dins d'un mateix mètode: decidir què significa la dada (quants dies queden, i a partir de quants dies alguna cosa es considera "crítica"?) i decidir com mostrar-la (quina etiqueta HTML, quina classe CSS). La segona versió separa totes dues preguntes en dos llocs diferents del cicle de vida, cadascun amb una responsabilitat clara: willUpdate decideix el significat, render() decideix la representació. Aquesta separació, encara que sembli un simple reordenament del mateix codi, facilita raonar sobre cada meitat per separat —i, no per casualitat, és exactament la mateixa separació que ja es va explicar, amb un altre exemple, a la lliçó "Hooks Reactius" del mòdul 6.
- Components descompostos enfront de components que acumulen responsabilitats
<task-card>, al llarg del curs, podria haver crescut com una única classe monolítica que dibuixés directament, dins del seu propi render(), tant l'avatar de la persona assignada com el filtre de cerca de tota l'aplicació. En el seu lloc, el curs ha anat extraient, en el moment en que cada responsabilitat es tornava suficientment diferent, un component propi: <user-avatar> al mòdul 4, quan mostrar una persona assignada (amb imatge o inicials, amb el seu propi <slot> i les seves pròpies regles d'estil) va deixar de ser un simple detall visual de <task-card> i va passar a merèixer la seva pròpia classe reutilitzable; <task-filter> al mòdul 7, quan gestionar el filtre de cerca va deixar de ser alguna cosa que <task-list> pogués continuar absorbint sense mesclar dues responsabilitats alienes entre elles (mostrar tasques, d'una banda; decidir quines mostrar, de l'altra).
| Senyal de que convé extreure un component | Exemple ja aplicat a TaskFlow |
|---|---|
| Una peça de la plantilla té el seu propi cicle d'estils, independent de la resta | <user-avatar>, amb la seva pròpia variable CSS --tamano-avatar |
| Una peça de comportament es podria reutilitzar en un context diferent de l'actual | <user-avatar>, pensat per poder aparèixer en qualsevol lloc de TaskFlow que necessiti mostrar una persona, no només dins de <task-card> |
| Dues responsabilitats dins de la mateixa classe canvien per motius diferents i en moments diferents | <task-filter> canvia quan canvien les regles de filtratge; <task-list> canvia quan canvia com es mostren les tasques |
| El nom d'una classe comença a necessitar la conjunció "i" per descriure's ("la llista que també filtra i també pagina") | Evitat extraient <task-filter> en lloc d'absorbir el filtre dins de <task-list> |
L'antipatró simètric a evitar és el contrari: extreure un component nou per cada detall mínim, sense que existeixi cap de les senyals de la taula anterior, multiplicant el nombre de fitxers i de Shadow DOMs sense cap guany real de claredat ni de reutilització. El criteri, igual que amb la resta d'aquesta lliçó, no és "descompondre sempre" ni "descompondre mai", sinó reconèixer les senyals concretes que justifiquen l'extracció, tal com TaskFlow ho ha fet en tots dos casos de la taula.
- Neteja de recursos enfront de fuites a
disconnectedCallback i controladors
disconnectedCallback i controladorsL'últim parell d'aquesta lliçó connecta directament els mòduls 6 i 9: la lliçó "Callbacks del Cicle de Vida" va advertir sobre no netejar un setInterval a disconnectedCallback, i la lliçó "Controladors Reactius" va repetir la mateixa advertència, ara sobre hostDisconnected, assenyalant que el risc es multiplica per cada host que utilitzi el controlador.
// Antipatró: arrenca el temporitzador, mai el detén
connectedCallback() {
super.connectedCallback();
this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}
// (sense disconnectedCallback, o amb un que no crida clearInterval)
// Patró recomanat: simetria entre connexió i desconnexió
connectedCallback() {
super.connectedCallback();
this._idIntervalo = setInterval(() => this._comprobar(), 60000);
}
disconnectedCallback() {
clearInterval(this._idIntervalo);
super.disconnectedCallback();
}Una <task-card> que s'elimina del DOM (per exemple, perquè el filtre de <task-filter> l'exclou del resultat) sense que el seu temporitzador s'aturi continua executant _comprobar() cada minut, indefinidament, encara que ja no existeixi cap node visible al qual aquesta comprovació pugui afectar; amb centenars de targetes creades i destruïdes al llarg d'una sessió llarga d'ús (l'escenari d'una llista gran explorat a la lliçó anterior), aquests temporitzadors orfes s'acumulen l'un darrere l'altre, consumint memòria i cicles de procés sense cap benefici, un problema que només es manifesta amb el temps i que rarament apareix en una prova manual ràpida de cinc minuts, precisament el tipus de fallada que una bateria de proves automatitzades com la de la lliçó 09-01 tampoc detecta si no es dissenya específicament per comprovar-ho.
- Un criteri final: preguntes que detecten un antipatró abans d'escriure'l
Els cinc parells anteriors comparteixen un tret comú que convé convertir en hàbit, no només en una llista per consultar després d'escriure el codi: cada antipatró correspon a una pregunta concreta que, formulada a temps, l'hauria evitat.
| Pregunta que convé fer-se | Antipatró que detecta |
|---|---|
| Estic substituint aquesta propietat per un valor nou, o modificant el valor que ja tenia? | Mutació al lloc (apartat 3) |
| Si aquest component té Shadow DOM, aquest esdeveniment necessita sortir-ne? | Falta de composed: true (apartat 4) |
| Aquest càlcul depèn només de propietats que ja han canviat, o es repeteix sense condició? | Lògica de negoci a render() (apartat 5) |
| Aquesta classe canvia per més d'un motiu diferent? | Responsabilitats sense descompondre (apartat 6) |
| Per cada recurs que arrenco, on exactament el vaig a aturar? | Fuita de recursos (apartat 7) |
- Cap al projecte final: repassar TaskFlow de principi a fi
Amb aquesta síntesi, el mòdul 9 completa la cobertura de qualitat que TaskFlow necessitava abans de considerar-se una aplicació acabada: proves automatitzades que verifiquen el seu comportament sense dependre de comprovacions manuals, accessibilitat per a qui no utilitza un ratolí sobre una pantalla convencional, rendiment revisat amb criteri enfront de volums de dades reals, i ara un catàleg de patrons i antipatrons que resumeix, d'una ullada, les decisions de disseny dels vuit mòduls anteriors.
Allò que queda pendent no és cap concepte nou de Lit —el curs ja ha cobert plantilles, reactivitat, estils, esdeveniments, cicle de vida, directives, context, integració, proves i bones pràctiques—, sinó recórrer TaskFlow de principi a fi com a aplicació completa: repassar cada component construït al llarg del curs, identificar quines peces van quedar mencionades però no completament resoltes, i acabar d'ensamblar-les en la versió final i acabada de l'aplicació. Aquesta és, precisament, la tasca de l'última lliçó del curs, "Projecte: Construint TaskFlow", el mòdul de tancament que dona per acabat el recorregut complet des del primer component Lit de la lliçó 01-03 fins a l'aplicació final.
Errors Comuns i Consells
- Tractar aquesta lliçó com una llista de regles aïllades, en lloc d'un criteri transferible: els cinc parells de l'apartat 2 no són una llista tancada d'errors específics de TaskFlow, sinó exemples concrets d'un grapat de preguntes generals (apartat 8) que es poden aplicar a qualsevol component Lit futur, inclòs qualsevol que s'afegeixi al projecte final.
- Aplicar un patró recomanat sense entendre quin antipatró evita: copiar
bubbles: true, composed: trueen tots els esdeveniments "per costum", sense saber que resol el problema concret de travessar el Shadow DOM, deixa a qui escriu el codi sense criteri per decidir quan, alguna vegada, cap de les dues opcions faria falta (un esdeveniment que un component només necessita escoltar sobre si mateix, sense cap pare interessat). - Confondre "descompondre en components" amb una regla numèrica: com s'ha assenyalat a l'apartat 6, no existeix un nombre correcte de components per aplicació; les senyals de la taula de l'apartat 6, no un recompte arbitrari, són el criteri que hauria de decidir quan extreure una peça nova.
- Revisar els antipatrons només al final d'un projecte, no durant el seu desenvolupament: aquesta lliçó arriba, deliberadament, abans del projecte final, no després; l'objectiu és que les preguntes de l'apartat 8 es converteixin en hàbit mentre es construeix el mòdul 10, no en una llista de comprovació aplicada a corre-cuita un cop acabat.
Exercicis
- Repassa el
render()de<task-board>tal com va quedar després de la lliçó "Mixins i Composició de Comportament" del mòdul 6 (ambConEstadoCarga) i assenyala, amb el criteri de l'apartat 5 d'aquesta lliçó, sithis.conIndicadorDeCarga(...)compta com "lògica de negoci dins derender()" o si encaixa millor amb el patró recomanat. Justifica la resposta. - Un company d'equip, revisant
<task-filter>, troba quemanejarTextoimanejarEstadocridenthis.valorActual.actualizar({...}), que al seu torn reassignathis._filtroProvider.valuea<task-board>amb l'operador de propagació ({ ...this._filtroProvider.value, ...cambios }, vist a la lliçó "Context Compartit amb @lit/context"). Explica, amb el criteri de l'apartat 3, per què aquesta reassignació compta com el patró recomanat i no com l'antipatró de mutació al lloc. - Aplicant les cinc preguntes de l'apartat 8, identifica quina d'elles hauria assenyalat el problema si
ContadorTiempoRestanteController(mòdul 6) hagués assignat directamentthis.host.cercaDeVencer = ...en lloc de mantenir el seu propi campcercaDeVenceri cridarthis.host.requestUpdate(), tal com es va advertir als "Errors Comuns" de la lliçó "Controladors Reactius".
Solucions
this.conIndicadorDeCarga(...)no és lògica de negoci dins derender()en el sentit assenyalat per l'apartat 5: no calcula ni deriva cap dada nova a partir de propietats (no decideix "què significa"cargando, aquest valor ja arriba calculat com una propietat booleana simple); es limita a traduir un valor ja existent (this.cargando) en una de dues plantilles alternatives, exactament la responsabilitat que l'apartat 5 reserva per arender(). SiconIndicadorDeCargacalculés, en canvi, alguna cosa com "quant temps porta carregant" a partir d'una marca de temps, aquest càlcul sí que encaixaria millor awillUpdate, però no és el que fa en la seva forma actual.- La reassignació
this._filtroProvider.value = { ...this._filtroProvider.value, ...cambios }segueix exactament el patró recomanat de l'apartat 3: en lloc de modificar l'objecte de context existent afegint-li o canviant-li una propietat al lloc (this._filtroProvider.value.texto = cambios.texto, cosa que mutaria l'objecte sense canviar la seva referència), crea un objecte completament nou mitjançant l'operador de propagació, combinant el valor anterior amb els canvis rebuts, i assigna aquest objecte nou a.value. Aquesta assignació canvia la referència del valor de context, exactament allò queContextProvidernecessita per notificar correctament els consumidors subscrits, el mateix mecanisme de detecció per referència explicat per a propietats reactives normals a l'apartat 3. - La pregunta que ho hauria assenyalat és la primera: "Estic substituint aquesta propietat per un valor nou, o modificant el valor que ja tenia?", encara que aplicada aquí no a una mutació d'array, sinó a la relació entre el controlador i el seu host. Assignar directament
this.host.cercaDeVencer = ...des de dins del controlador acobla el mecanisme intern del controlador a una propietat reactiva concreta que el host hauria de declarar només per donar cabuda a aquest detall d'implementació, exactament el mateix tipus d'acoblament innecessari que l'apartat 6 desaconsella entre responsabilitats que haurien de romandre separades: el controlador decideix el seu propi resultat; el host, a través derequestUpdate(), només s'assabenta de que ha de tornar a renderitzar-se, sense que totes dues responsabilitats es mesclin en una única propietat compartida.
Conclusió
Aquesta lliçó ha revisat, en cinc parells, els antipatrons més assenyalats al llarg del curs enfront de l'alternativa recomanada que TaskFlow ha aplicat en cada cas: immutabilitat en lloc de mutació al lloc, composed: true perquè els esdeveniments travessin el Shadow DOM, càlcul derivat fora de render(), components descompostos segons senyals concretes, i neteja simètrica de qualsevol recurs arrencat al cicle de vida. Amb aquest repàs, i amb les proves, l'accessibilitat i el rendiment de les tres lliçons anteriors, el mòdul 9 completa la cobertura de qualitat de TaskFlow.
Amb això es tanca també el contingut teòric del curs sencer. Queda una única lliçó, l'última del curs: "Projecte: Construint TaskFlow", on no es presenta cap concepte nou, sinó que es recorre l'aplicació de principi a fi, es repassen i completen les peces que al llarg del curs van quedar mencionades sense acabar del tot, i s'entrega TaskFlow com l'aplicació acabada que corona tot el recorregut, des del primer component Lit fins al projecte final.
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
