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

  1. El problema de fons: prop drilling i components sense relació directa
  2. createContext: definint una clau de context
  3. ContextProvider: publicant un valor des de l'avantpassat
  4. ContextConsumer: llegint el valor des de qualsevol descendent
  5. Dissenyant el context de filtre de TaskFlow
  6. <task-board> com a proveïdor
  7. <task-filter>: el component que faltava
  8. <task-list> com a consumidor: filtrant de veritat
  9. Context davant de pujar l'estat: el criteri final

  1. 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.

  1. 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.

  1. ContextProvider: publicant un valor des de l'avantpassat

Un 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:

this._filtroProvider.value = { texto: 'diseño', estado: 'todas' };

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.

  1. ContextConsumer: llegint el valor des de qualsevol descendent

Un 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.

  1. 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:

{
  texto: '',
  estado: 'todas',
  actualizar(cambios) { /* ... */ },
}

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.

  1. <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.

  1. <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.

  1. <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.

  1. 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: true en un ContextConsumer: 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ó a this._filtroProvider.value substitueix l'objecte complet; si _actualizarFiltro oblidés incloure actualizar al nou objecte, qualsevol consumidor que després cridés this.valorActual.actualizar(...) fallaria, perquè aquesta funció ja no existiria al nou valor.
  • Utilitzar @lit/context per 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> passant tareas a <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.value queda simplement undefined, sense cap error explícit; per això valorActual i tareasFiltradas, 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

  1. Afegeix al valor de context de filtre un tercer camp, prioridadMinima (amb 0 com a valor per defecte), i modifica tareasFiltradas a <task-list> per excloure també les tasques amb tarea.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.
  2. Explica, basant-te en l'apartat 3, per què ContextProvider necessita rebre el host (this) com a primer argument del seu constructor, recolzant-te en el paral·lelisme assenyalat amb ContadorTiempoRestanteController de la lliçó 06-03.
  3. Un company d'equip proposa que, en lloc d'incloure la funció actualizar dins del propi valor de context (apartat 5), <task-filter> despatxi un CustomEvent amb bubbles: 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;
  });
}
  1. ContextProvider, igual que ContadorTiempoRestanteController, 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 a ReactiveController (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/context localitza 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.
  2. 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 un CustomEvent, <task-filter> continuaria sense necessitar conèixer <task-board> directament (gràcies a bubbles/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ó actualizar genè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

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