La lliçó 05-04 va deixar una peça de TaskFlow pendent a propòsit: <task-filter>, un component germà de <task-list> que hauria de permetre filtrar les tasques visibles, però que en aquell moment no es podia implementar sense recórrer a un patró incòmode —reenviar manualment el text de filtre des de <task-filter> cap a <task-board>, i de <task-board> cap a <task-list>, en dos salts separats— o sense avançar contingut d'aquest mòdul. Aquella mateixa lliçó va esmentar, de passada, que existia una alternativa més elegant: @lit/context. Aquesta lliçó explica aquesta alternativa amb detall i, amb ella, implementa finalment <task-filter>.
Contingut
- El problema de fons: prop drilling i components sense relació directa
createContext: definint una clau de contextContextProvider: publicant un valor des de l'avantpassatContextConsumer: llegint el valor des de qualsevol descendent- Dissenyant el context de filtre de TaskFlow
<task-board>com a proveïdor<task-filter>: el component que faltava<task-list>com a consumidor: filtrant de veritat- Context davant de pujar l'estat: el criteri final
- El problema de fons: prop drilling i components sense relació directa
La lliçó 05-04 va resoldre la comunicació entre <task-list> i <task-board> pujant l'estat compartit (l'array tareas) a l'avantpassat comú, amb el cicle ja conegut d'esdeveniment cap amunt i propietat cap avall. Aquest patró funciona bé mentre la jerarquia sigui petita, però deixa dues preguntes obertes que aquella lliçó va esmentar sense respondre: què passa quan la dada compartida ha de travessar diversos nivells intermedis que ni tan sols la utilitzen, només per arribar d'un extrem a l'altre (el problema conegut com prop drilling, "perforació de propietats")? I què passa quan dos components sense cap relació d'avantpassat comú proper —com <task-filter> i <task-list>, tots dos germans directes de <task-board>, cadascun amb la seva pròpia responsabilitat— necessiten compartir una dada que, a més, un dels dos necessita actualitzar, no només llegir?
<task-filter> és exactament aquest segon cas: necessita escriure un valor (el text o l'estat de filtre triat per l'usuari), i <task-list> necessita llegir aquest mateix valor per decidir quines tasques mostrar, sense que cap dels dos tingui una referència directa a l'altre. Resoldre-ho amb el patró de la lliçó 05-04 exigiria que <task-board> mantingués el filtre com a propietat d'estat pròpia, rebés un esdeveniment de <task-filter> cada cop que canviï, i reenviés el nou valor com a propietat cap a <task-list>: un cicle perfectament vàlid, però que converteix <task-board> en un intermediari obligat d'una dada que, en realitat, no utilitza per a res per si mateix, només per passar-la d'un fill a un altre.
createContext: definint una clau de context
createContext: definint una clau de context@lit/context és un paquet independent del nucli de Lit (s'instal·la per separat, amb npm install @lit/context), pensat específicament per al problema de l'apartat anterior: permet que un component avantpassat publiqui un valor, i que qualsevol descendent, a qualsevol profunditat, el consumeixi directament, sense que cap nivell intermedi necessiti reenviar res manualment.
El primer pas és definir una clau de context, normalment en un fitxer compartit que tant el proveïdor com els consumidors puguin importar:
// src/contexts/filtro-context.js
import { createContext } from '@lit/context';
export const filtroContext = createContext('filtro-tareas');createContext(...) no crea el valor compartit en si mateix, sinó un identificador únic que actua de clau per reconèixer aquest context concret entre el proveïdor i els seus consumidors; l'argument ('filtro-tareas', en aquest cas) és només una descripció llegible pensada per a depuració, no un valor que un altre codi pugui utilitzar per "endevinar" o falsificar la clau des de fora. Un mateix projecte pot definir tants contextos independents com necessiti, cadascun amb el seu propi createContext(...) en el seu propi mòdul, exactament com TaskFlow defineix aquí un de sol, dedicat en exclusiva al filtre de tasques.
ContextProvider: publicant un valor des de l'avantpassat
ContextProvider: publicant un valor des de l'avantpassatUn component es converteix en proveïdor d'un context instanciant la classe ContextProvider, importada també de @lit/context, normalment en el seu propi constructor:
import { ContextProvider } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
class TaskBoard extends LitElement {
constructor() {
super();
this._filtroProvider = new ContextProvider(this, {
context: filtroContext,
initialValue: { texto: '', estado: 'todas' },
});
}
}ContextProvider rep el propi component (this) com a host, i un objecte de configuració amb la clau de context (context: filtroContext) i un valor inicial. Qui ja conegui el patró dels controladors reactius, presentat a la lliçó 06-03, hi reconeixerà un disseny molt semblant: ContextProvider, igual que ContadorTiempoRestanteController en el seu moment, es registra sobre un host rebut com a primer argument i s'engancha al seu cicle de vida sense que TaskBoard necessiti heretar de cap classe especial per a això. No és una coincidència d'estil: ContextProvider està, de fet, implementat internament com un ReactiveController, la mateixa interfície estudiada al mòdul 6, reutilitzada aquí pel propi equip de Lit per resoldre un problema diferent amb la mateixa peça d'infraestructura.
Per actualitzar el valor publicat més endavant (per exemple, quan l'usuari canvia el filtre), n'hi ha prou amb assignar la propietat value del proveïdor:
Aquesta assignació notifica automàticament qualsevol consumidor subscrit a aquest mateix context, sense que TaskBoard necessiti conèixer quants consumidors hi ha ni on estan col·locats a l'arbre de components.
ContextConsumer: llegint el valor des de qualsevol descendent
ContextConsumer: llegint el valor des de qualsevol descendentUn component llegeix un context instanciant, de forma simètrica, la classe ContextConsumer:
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
class TaskList extends LitElement {
constructor() {
super();
this._filtro = new ContextConsumer(this, {
context: filtroContext,
subscribe: true,
});
}
}this._filtro.value exposa, en tot moment, el valor actual publicat pel proveïdor més proper cap amunt a l'arbre de components que utilitzi la mateixa clau de context (filtroContext), sense que <task-list> necessiti cap referència directa a <task-board> ni conèixer en quin nivell exacte de la jerarquia es troba aquest proveïdor. L'opció subscribe: true és imprescindible si es vol que el consumidor continuï rebent actualitzacions cada cop que el proveïdor canviï el seu valor al llarg del temps (el cas de TaskFlow, on el filtre canvia repetidament mentre l'usuari escriu o prem botons); sense ella, ContextConsumer només obtindria el valor disponible en el moment de crear-se, ignorant qualsevol actualització posterior del proveïdor, cosa que rarament és el que es busca en un context pensat per a dades que canvien amb el temps.
- Dissenyant el context de filtre de TaskFlow
Amb la teoria ja coberta, toca decidir quina forma ha de tenir exactament el valor compartit per filtroContext. TaskFlow necessita dues peces d'informació —un text de cerca i un estat seleccionat ('todas', 'pendiente' o 'hecha')— i, a més, una forma perquè <task-filter> pugui actualitzar aquest valor sense necessitat que <task-board> conegui <task-filter> de cap forma especial. La solució més senzilla, atès que un valor de context pot ser qualsevol valor de JavaScript, inclòs un objecte amb mètodes, és incloure la pròpia funció d'actualització com a part del valor compartit:
Qualsevol consumidor d'aquest context rep, juntament amb les dades, una referència a la mateixa funció actualizar, que pot invocar directament per modificar el filtre, sense necessitat d'un segon canal de comunicació (com un esdeveniment personalitzat) per a la direcció "de tornada" cap al proveïdor.
<task-board> com a proveïdor
<task-board> com a proveïdor// src/components/task-board.js
import { LitElement, html, css } from 'lit';
import { ContextProvider } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
import './task-list.js';
import './task-filter.js';
class TaskBoard extends LitElement {
static properties = {
tareas: { type: Array },
};
constructor() {
super();
this.tareas = [];
this._filtroProvider = new ContextProvider(this, {
context: filtroContext,
initialValue: {
texto: '',
estado: 'todas',
actualizar: (cambios) => this._actualizarFiltro(cambios),
},
});
}
_actualizarFiltro(cambios) {
this._filtroProvider.value = {
...this._filtroProvider.value,
...cambios,
};
}
render() {
return html`
<div class="tablero">
<h1>TaskFlow</h1>
<task-filter></task-filter>
<task-list .tareas="${this.tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
</div>
`;
}
}
customElements.define('task-board', TaskBoard);_actualizarFiltro(cambios) fusiona els canvis rebuts (per exemple, { texto: 'diseño' }, sense tocar estado) amb el valor actual del context, i reassigna this._filtroProvider.value amb l'objecte combinat complet, incloent-hi de nou la pròpia funció actualizar (necessària a cada nou valor, ja que se substitueix l'objecte sencer, no només algun dels seus camps). Cal notar un detall important que distingeix aquest objecte d'una propietat reactiva corrent: _actualizarFiltro no està declarat a static properties de TaskBoard, perquè no és una dada que el propi <task-board> necessiti llegir al seu render(); és responsabilitat exclusiva de ContextProvider decidir quan notificar els consumidors, cada cop que s'assigna .value.
Amb aquesta peça, <task-board> ja no reenvia cap propietat de filtre cap a <task-filter> ni cap a <task-list>: tots dos es col·loquen simplement com a fills directes a la plantilla, sense cap atribut ni propietat relacionada amb el filtre, exactament l'objectiu que la lliçó 05-04 va deixar pendent.
<task-filter>: el component que faltava
<task-filter>: el component que faltava// src/components/task-filter.js
import { LitElement, html, css } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
class TaskFilter extends LitElement {
constructor() {
super();
this._filtro = new ContextConsumer(this, { context: filtroContext, subscribe: true });
}
get valorActual() {
return this._filtro.value ?? { texto: '', estado: 'todas', actualizar: () => {} };
}
manejarTexto(evento) {
this.valorActual.actualizar({ texto: evento.target.value });
}
manejarEstado(estado) {
this.valorActual.actualizar({ estado });
}
render() {
const { texto, estado } = this.valorActual;
return html`
<div class="filtro">
<input
type="text"
placeholder="Buscar tarea…"
.value="${texto}"
@input="${this.manejarTexto}"
/>
<div class="filtro__botones">
${['todas', 'pendiente', 'hecha'].map(
(opcion) => html`
<button
class="${classMap({ activo: estado === opcion })}"
@click="${() => this.manejarEstado(opcion)}"
>
${{ todas: 'Todas', pendiente: 'Pendientes', hecha: 'Hechas' }[opcion]}
</button>
`
)}
</div>
</div>
`;
}
static styles = css`
.filtro__botones button.activo {
font-weight: bold;
border-bottom: 2px solid currentColor;
}
`;
}
customElements.define('task-filter', TaskFilter);<task-filter> no declara cap propietat reactiva pròpia per a texto ni estado: els llegeix directament de this._filtro.value a cada render(), i quan l'usuari escriu a l'<input> o prem un botó, crida this.valorActual.actualizar(...), la mateixa funció que <task-board> va publicar com a part del valor de context a l'apartat 6. Cal notar com aquesta lliçó enllaça amb l'anterior: els botons d'estat utilitzen classMap, presentada a la primera lliçó d'aquest mòdul, per alternar la classe activo segons quina de les tres opcions coincideixi amb estado, sense necessitat de tres blocs de plantilla gairebé idèntics.
Quan manejarTexto o manejarEstado criden actualizar(...), aquesta crida executa finalment _actualizarFiltro a <task-board> (apartat 6), que reassigna this._filtroProvider.value; ContextProvider s'encarrega, automàticament, de notificar tots els consumidors subscrits —inclòs el propi <task-filter>, i <task-list>, com es veu a l'apartat següent— perquè es tornin a renderitzar amb el nou valor.
<task-list> com a consumidor: filtrant de veritat
<task-list> com a consumidor: filtrant de veritat// src/components/task-list.js
import { LitElement, html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { ContextConsumer } from '@lit/context';
import { filtroContext } from '../contexts/filtro-context.js';
import './task-card.js';
class TaskList extends LitElement {
static properties = {
tareas: { type: Array },
};
constructor() {
super();
this.tareas = [];
this._filtro = new ContextConsumer(this, { context: filtroContext, subscribe: true });
}
get tareasFiltradas() {
const { texto, estado } = this._filtro.value ?? { texto: '', estado: 'todas' };
const textoNormalizado = texto.toLowerCase();
return this.tareas.filter((tarea) => {
const coincideEstado = estado === 'todas' || tarea.estado === estado;
const coincideTexto = tarea.titulo.toLowerCase().includes(textoNormalizado);
return coincideEstado && coincideTexto;
});
}
render() {
return html`
<ul>
${repeat(
this.tareasFiltradas,
(tarea) => tarea.id,
(tarea) => html`
<li>
<task-card
titulo="${tarea.titulo}"
estado="${tarea.estado}"
prioridad="${tarea.prioridad}"
></task-card>
</li>
`
)}
</ul>
`;
}
}
customElements.define('task-list', TaskList);tareasFiltradas combina les dues condicions del filtre —coincidència d'estat i coincidència de text, en minúscules perquè la cerca no distingeixi majúscules— i render() utilitza aquest resultat, ja filtrat, en lloc de l'array tareas complet. Aquest és, a més, el moment en què la directiva repeat, esmentada de passada des de la lliçó 02-04, troba finalment el seu cas d'ús natural a TaskFlow: cada canvi de filtre modifica quines tasques són visibles, inserint i eliminant elements de la llista renderitzada en temps real; amb Array.map, cada canvi de filtre podria fer que Lit comparés per posició i reconstruís o reordenés targetes que en realitat són les mateixes d'abans, arriscant l'estat intern de qualsevol <task-card> que estigués expandida (recordada com a estat intern des del mòdul 3); amb repeat i tarea.id com a clau, cada targeta conserva la seva identitat i el seu estat mentre continuï complint el filtre, i només es creen o destrueixen nodes per a les tasques que realment entren o surten del resultat filtrat.
Amb això, <task-board> no reenvia absolutament res relacionat amb el filtre entre <task-filter> i <task-list>: tots dos llegeixen i escriuen el mateix context de forma directa, cadascun amb la seva pròpia responsabilitat —un l'actualitza, l'altre el consumeix per decidir què mostrar— sense conèixer-se entre si en cap sentit.
- Context davant de pujar l'estat: el criteri final
Amb @lit/context ja aplicat, convé tancar la comparació que la lliçó 05-04 va deixar oberta:
| Criteri | Pujar l'estat (05-04) | @lit/context (aquesta lliçó) |
|---|---|---|
| On viu la dada compartida | En una propietat de l'avantpassat comú, reenviada explícitament cap avall | En un ContextProvider, accessible directament per qualsevol descendent subscrit |
| Els nivells intermedis necessiten reenviar la dada? | Sí, cada nivell que estigui entre el proveïdor lògic i el consumidor | No: qualsevol descendent a qualsevol profunditat hi accedeix directament |
| Complexitat per a una jerarquia de dos o tres nivells | Baixa: el patró d'esdeveniment amunt / propietat avall és directe i fàcil de seguir | Lleugerament més gran: cal definir el context, el proveïdor i cada consumidor per separat |
| Complexitat per a jerarquies més profundes o amb germans que no comparteixen avantpassat immediat | Creix amb cada nivell intermedi que només fa d'intermediari | Es manté constant, independentment de la profunditat |
| Exemple d'aquest curs | <task-board> → <task-list> → <task-card>, amb esdeveniments tarea-cambiada reenviats |
El filtre entre <task-filter> i <task-list>, germans sense relació directa |
El criteri, tal com ja apuntava la lliçó 05-04, no ha canviat: per a jerarquies petites i relacions directes de pare a fill, pujar l'estat continua sent més senzill de seguir i depurar, precisament perquè el flux de dades és explícit a cada nivell de l'arbre de components. @lit/context demostra el seu valor quan, com en el cas de <task-filter> i <task-list>, dos components sense relació directa necessiten compartir una dada sense que un tercer aliè hagi de fer d'intermediari obligat.
Errors Habituals i Consells
- Oblidar
subscribe: trueen unContextConsumer: com s'ha explicat a l'apartat 4, sense aquesta opció el consumidor només rep el valor disponible en el moment de crear-se, i mai més es torna a actualitzar encara que el proveïdor canviï el seu valor més endavant; el símptoma és un component que sembla "congelat" amb el primer valor de filtre, sense reaccionar als canvis de l'usuari. - Reassignar només part de l'objecte de context, perdent la funció
actualizar: com s'ha assenyalat a l'apartat 6, cada assignació athis._filtroProvider.valuesubstitueix l'objecte complet; si_actualizarFiltrooblidés incloureactualizaral nou objecte, qualsevol consumidor que després cridésthis.valorActual.actualizar(...)fallaria, perquè aquesta funció ja no existiria al nou valor. - Utilitzar
@lit/contextper a dades que només necessita un únic fill directe: com recorda l'apartat 9, per a una relació senzilla de pare a fill (com<task-board>passanttareasa<task-list>), una propietat normal continua sent més senzilla i explícita; introduir un context aquí només afegeix indirecció sense resoldre cap problema real de prop drilling. - Esperar que un consumidor sense proveïdor actiu llenci un error clar: si cap avantpassat d'un component proporciona el context que intenta consumir,
this._filtro.valuequeda simplementundefined, sense cap error explícit; per aixòvalorActualitareasFiltradas, als apartats 7 i 8, utilitzen?? { ... }per oferir un valor per defecte raonable en aquest cas, en lloc d'assumir que el context sempre estarà disponible.
Exercicis
- Afegeix al valor de context de filtre un tercer camp,
prioridadMinima(amb0com a valor per defecte), i modificatareasFiltradasa<task-list>per excloure també les tasques ambtarea.prioridad < prioridadMinima. No cal afegir cap control visual nou a<task-filter>per a aquest exercici, n'hi ha prou amb la lògica de filtratge. - Explica, basant-te en l'apartat 3, per què
ContextProvidernecessita rebre el host (this) com a primer argument del seu constructor, recolzant-te en el paral·lelisme assenyalat ambContadorTiempoRestanteControllerde la lliçó 06-03. - Un company d'equip proposa que, en lloc d'incloure la funció
actualizardins del propi valor de context (apartat 5),<task-filter>despatxi unCustomEventambbubbles: true, composed: true(com a la lliçó 05-02) que<task-board>escolti per actualitzar el filtre. Explica quin avantatge conserva l'enfocament d'aquesta lliçó (la funció dins del context) davant d'aquesta alternativa, en termes de què necessita saber<task-filter>sobre la seva posició a l'arbre de components.
Solucions
get tareasFiltradas() {
const { texto, estado, prioridadMinima = 0 } = this._filtro.value ?? { texto: '', estado: 'todas' };
const textoNormalizado = texto.toLowerCase();
return this.tareas.filter((tarea) => {
const coincideEstado = estado === 'todas' || tarea.estado === estado;
const coincideTexto = tarea.titulo.toLowerCase().includes(textoNormalizado);
const coincidePrioridad = tarea.prioridad >= prioridadMinima;
return coincideEstado && coincideTexto && coincidePrioridad;
});
}ContextProvider, igual queContadorTiempoRestanteController, necessita el host per dos motius idèntics als explicats a la lliçó 06-03: primer, per registrar-se sobre el seu cicle de vida com aReactiveController(de manera que sàpiga quan el component es connecta o desconnecta del DOM, rellevant per començar o deixar d'escoltar peticions de context des dels descendents); segon, per saber en quin node exacte de l'arbre de components s'ha d'ancorar com a proveïdor, ja que@lit/contextlocalitza el proveïdor més proper recorrent el DOM cap amunt des de cada consumidor, i aquest recorregut necessita un punt de partida real a l'arbre, que és precisament el host rebut com a primer argument.- Amb l'enfocament d'aquesta lliçó,
<task-filter>no necessita saber res sobre la seva posició a l'arbre de components ni sobre qui, en algun nivell superior, reaccionarà als seus canvis: simplement crida una funció que rep com a part del valor de context, sense cap suposició sobre quin component la implementa ni a quina distància es troba. Amb unCustomEvent,<task-filter>continuaria sense necessitar conèixer<task-board>directament (gràcies abubbles/composed, com a la lliçó 05-02), però<task-board>hauria de declarar explícitament un gestor d'esdeveniments per a cada tipus de canvi de filtre, mentre que amb el context d'aquesta lliçó n'hi ha prou amb una única funcióactualizargenèrica, capaç d'acceptar qualsevol combinació de canvis ({ texto: ... },{ estado: ... }, o tots dos alhora) sense necessitat d'un tipus d'esdeveniment diferent per a cada camp del filtre.
Conclusió
Aquesta lliçó ha tancat, finalment, l'esment pendent des de la lliçó 05-04: @lit/context, amb createContext, ContextProvider i ContextConsumer, ha resolt la comunicació entre <task-filter> i <task-list> sense relació directa entre tots dos i sense que <task-board> hagués de reenviar manualment cap propietat de filtre. Amb aquesta peça, TaskFlow completa la seva jerarquia de components tal com s'havia anat anunciant des del mòdul 5: <task-board> com a orquestrador i únic proveïdor de context, <task-list> filtrant i renderitzant amb repeat les tasques visibles, <task-card> (amb <user-avatar> a dins) mostrant cada tasca amb les seves insígnies d'estat i la seva urgència calculada per ContadorTiempoRestanteController, i <task-filter>, el component que portava tres mòduls esmentat sense implementar, actualitzant el filtre compartit directament a través del context.
Amb TaskFlow funcionalment complet, toca veure com s'integra amb la resta del món: HTML pla, altres frameworks, SSR i empaquetatge.
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
