Les dues lliçons anteriors han resolt completament la comunicació entre <task-card> i <task-list>: un esdeveniment puja, una propietat baixa, i ambdós components es mantenen sincronitzats sense que cap conegui l'estructura interna de l'altre. Aquest patró funciona perquè <task-card> i <task-list> tenen una relació directa de pare i fill. Però TaskFlow està a punt de necessitar una cosa diferent: un futur component <task-filter>, amb controls per filtrar la llista de tasques visible, que no és fill de <task-list> ni al revés, sinó que tots dos hauran de convivir com a germans sota un mateix contenidor. Aquesta lliçó presenta el patró estàndard per resoldre la comunicació entre components sense relació directa, i l'aplica creant <task-board>, el component orquestrador que donarà cabuda, a partir d'ara, a tota l'estructura de TaskFlow.

Contingut

  1. El problema: dos germans que necessiten parlar entre si
  2. El patró d'"elevar l'estat" a un ancestre comú
  3. Creant <task-board> com a orquestrador
  4. <task-board> escolta <task-list>
  5. Preparant l'espai per a <task-filter>
  6. Alternatives per a aplicacions més grans
  7. Tancament: cap al cicle de vida i les actualitzacions

  1. El problema: dos germans que necessiten parlar entre si

Imagina l'escenari, encara per construir en aquest curs, de <task-filter>: un component amb, per exemple, un <select> per triar "mostrar només tasques urgents" o "mostrar només tasques pendents". Quan l'usuari canviï aquest filtre, d'alguna manera <task-list> ha d'assabentar-se'n i deixar de mostrar les targetes que no compleixin el criteri triat. El problema és que, si ambdós components es col·loquen l'un al costat de l'altre a l'HTML de l'aplicació...

<task-filter></task-filter>
<task-list></task-list>

... no existeix cap relació de pare i fill entre ells que permeti aplicar directament els patrons ja vistos. <task-filter> no pot passar-li una propietat a <task-list> (les propietats, com es va recordar a la lliçó anterior, només viatgen d'un component als seus propis fills a la plantilla), i encara que <task-filter> despatxés un esdeveniment personalitzat amb bubbles: true i composed: true, aquest esdeveniment pujaria cap als seus ancestres comuns amb <task-list>, no cap a <task-list> directament, perquè els esdeveniments del DOM no tenen cap noció de "enviar això al meu germà": només pugen cap amunt, mai travessen lateralment entre branques diferents de l'arbre.

  1. El patró d'"elevar l'estat" a un ancestre comú

La solució estàndard, no exclusiva de Lit ni de Web Components (el mateix patró es coneix, amb el mateix nom, a React i a moltes altres biblioteques d'interfície basades en components), consisteix a elevar l'estat compartit a un ancestre comú d'ambdós germans: en lloc que <task-filter> i <task-list> intentin parlar directament entre si, un tercer component, pare de tots dos, manté la dada que ambdós necessiten compartir (en aquest cas, el criteri de filtre actiu), i es converteix en l'intermediari de tota la comunicació:

  • <task-filter>, quan l'usuari canvia el criteri, despatxa un esdeveniment personalitzat cap amunt (exactament el mateix patró "el fill anuncia" de la lliçó 05-02), però aquesta vegada qui l'escolta és l'ancestre comú, no <task-list> directament.
  • L'ancestre comú actualitza el seu propi estat amb el nou criteri de filtre.
  • L'ancestre comú passa aquest criteri cap avall, com a propietat, a <task-list> (exactament el mateix patró "propietat cap al fill" de la lliçó 05-03).
  • <task-list> utilitza aquesta propietat per decidir quines tasques mostrar.

En cap moment <task-filter> i <task-list> es coneixen ni es comuniquen directament entre si: cadascun només parla amb el seu pare comú, que és qui coneix ambdós i decideix com coordinar la informació entre ells. Aquest és, en essència, el mateix patró de les dues lliçons anteriors (esdeveniment cap amunt, propietat cap avall), aplicat dues vegades consecutives —una vegada entre <task-filter> i l'ancestre, una altra vegada entre l'ancestre i <task-list>— en lloc d'una sola vegada entre dos components directament emparentats.

  1. Creant <task-board> com a orquestrador

TaskFlow necessita, per aplicar aquest patró, un nou component que faci d'ancestre comú: <task-board>, el tauler complet de l'aplicació, que a partir d'ara serà qui contingui <task-list> (i, més endavant en el curs, <task-filter>), i qui mantingui l'estat que tots dos necessitin compartir.

// src/components/task-board.js
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './task-list.js';

class TaskBoard extends LitElement {
  static properties = {
    tareas: { type: Array },
  };

  static styles = [
    estilosCompartidos,
    css`
      .tablero {
        display: flex;
        flex-direction: column;
        gap: 1rem;
        padding: 1rem;
      }
    `,
  ];

  constructor() {
    super();
    this.tareas = [
      { id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
      { id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
      { id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
    ];
  }

  render() {
    return html`
      <div class="tablero">
        <h1>TaskFlow</h1>
        <task-list .tareas="${this.tareas}"></task-list>
      </div>
    `;
  }
}

customElements.define('task-board', TaskBoard);

Aquest canvi trasllada a <task-board> una responsabilitat que fins ara tenia <task-list>: l'array tareas complet, amb les seves dades inicials, ja no s'inicialitza dins de <task-list>, sinó a <task-board>, i baixa cap a <task-list> com a propietat, amb el binding de punt ja conegut (.tareas="${this.tareas}"). <task-list>, per la seva banda, deixa de tenir un constructor que inventa les seves pròpies dades d'exemple: a partir d'ara, rep sempre tareas des de fora, igual que <task-card> rep sempre titulo o estado des de <task-list>. Aquesta redistribució de responsabilitats és exactament el que fa falta perquè, més endavant, <task-filter> pugui incorporar-se al mateix nivell que <task-list>, tots dos com a fills de <task-board>, sense que cap dels dos necessiti mantenir per si sol l'array complet de tasques.

  1. <task-board> escolta <task-list>

L'esdeveniment tarea-cambiada, que a la lliçó 05-03 pujava de <task-card> a <task-list>, continua exactament igual: no canvia res a <task-card>. El que sí que canvia és on viu ara la lògica que decideix com actualitzar l'array tareas: com que aquest array ha passat a viure a <task-board>, és <task-board> qui s'ha d'encarregar d'actualitzar-lo quan una targeta canvia d'estat, i <task-list> passa a limitar-se a reenviar l'esdeveniment cap amunt, sense gestionar-lo per si sola:

// src/components/task-list.js
class TaskList extends LitElement {
  static properties = {
    tareas: { type: Array },
  };

  render() {
    return html`
      <section>
        <h2>Mis tareas</h2>
        <div class="lista">
          ${this.tareas.map(
            (tarea) => html`
              <task-card
                .titulo="${tarea.titulo}"
                .estado="${tarea.estado}"
                .prioridad="${tarea.prioridad}"
                .urgente="${tarea.urgente}"
                @tarea-cambiada="${(event) => this.reenviarTareaCambiada(tarea.id, event)}"
              ></task-card>
            `
          )}
        </div>
      </section>
    `;
  }

  reenviarTareaCambiada(idTarea, event) {
    this.dispatchEvent(
      new CustomEvent('tarea-cambiada', {
        detail: { idTarea, nuevoEstado: event.detail.nuevoEstado },
        bubbles: true,
        composed: true,
      })
    );
  }
}
// src/components/task-board.js
class TaskBoard extends LitElement {
  // ...properties, styles y constructor sin cambios...

  gestionarTareaCambiada(event) {
    const { idTarea, nuevoEstado } = event.detail;
    this.tareas = this.tareas.map((tarea) =>
      tarea.id === idTarea ? { ...tarea, estado: nuevoEstado } : tarea
    );
  }

  render() {
    return html`
      <div class="tablero">
        <h1>TaskFlow</h1>
        <task-list .tareas="${this.tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
      </div>
    `;
  }
}

Aquest reenviament mereix aturar-se un moment en ell, perquè és la peça nova d'aquesta lliçó: <task-list> rep tarea-cambiada d'una <task-card> concreta (sap, gràcies al tancament sobre tarea.id ja vist a la lliçó anterior, a quina tasca afecta), però en lloc de decidir ella mateixa com actualitzar l'array, construeix i despatxa un nou esdeveniment personalitzat, també anomenat tarea-cambiada, aquesta vegada amb idTarea inclòs explícitament al seu detail (una cosa que no calia a la versió original, perquè abans era la pròpia <task-list> qui ja coneixia tarea.id pel tancament del map; ara aquesta dada necessita viatjar explícitament a l'esdeveniment, perquè qui la consumirà, <task-board>, no té accés a aquesta variable de tancament). El nou esdeveniment, amb bubbles: true i composed: true igual que l'original, puja des de <task-list> fins a <task-board>, que és qui finalment aplica la lògica d'actualització immutable ja coneguda de la lliçó anterior.

És perfectament vàlid, i de fet habitual en aplicacions reals amb jerarquies més profundes, que un component intermedi com <task-list> no gestioni un esdeveniment per si mateix sinó que simplement el deixi passar cap amunt, afegint la informació addicional que faci falta pel camí. Aquest reenviament en cadena és, en essència, la mateixa idea de la bombolla nativa d'esdeveniments del DOM (vista a la lliçó 05-01), però aplicada de forma explícita i controlada entre diferents nivells de components personalitzats.

  1. Preparant l'espai per a <task-filter>

Amb <task-board> ja al seu lloc com a orquestrador, queda perfectament preparat el terreny perquè, quan es construeixi <task-filter> (una tasca que correspon a un mòdul posterior d'aquest curs), s'incorpori exactament al mateix nivell que <task-list>, com a germà sota el mateix <task-board>:

// Vista prèvia de com quedarà <task-board> més endavant al curs,
// un cop existeixi <task-filter> (encara no implementat en aquest mòdul)
render() {
  return html`
    <div class="tablero">
      <h1>TaskFlow</h1>
      <task-filter @filtro-cambiado="${this.gestionarFiltroCambiado}"></task-filter>
      <task-list
        .tareas="${this.tareasFiltradas()}"
        @tarea-cambiada="${this.gestionarTareaCambiada}"
      ></task-list>
    </div>
  `;
}

No fa falta implementar <task-filter> ni tareasFiltradas() en aquest mòdul (aquest desenvolupament arribarà més endavant en el curs, quan es disposi de més eines de plantilles); l'important, per a aquesta lliçó, és constatar que l'estructura que s'acaba de construir —<task-board> com a únic punt que coneix tant <task-list> com, en el futur, <task-filter>— ja és capaç de donar cabuda a aquest component addicional sense cap canvi d'arquitectura: <task-filter> despatxaria el seu propi esdeveniment cap amunt (per exemple, filtro-cambiado), <task-board> l'escoltaria i actualitzaria un nou estat intern o propietat amb el criteri actiu, i aquest criteri es combinaria amb this.tareas (per exemple, en un mètode tareasFiltradas()) abans de passar-lo cap a <task-list>. Ni <task-filter> ni <task-list> necessitarien, en cap moment, conèixer-se l'un a l'altre.

  1. Alternatives per a aplicacions més grans

El patró d'elevar l'estat a un ancestre comú, aplicat en aquesta lliçó amb un únic nivell de <task-board>, escala raonablement bé mentre l'aplicació no creixi massa en profunditat. Però convé saber que, en aplicacions més grans, amb jerarquies de molts nivells o amb molts components que necessiten la mateixa dada compartida, aquest patró pot tornar-se incòmode: si l'ancestre comú estigués diversos nivells per sobre, cada nivell intermedi hauria de reenviar esdeveniments cap amunt i propietats cap avall, com ha fet <task-list> a l'apartat 4, únicament per fer d'intermediari d'una dada que ni tan sols utilitza ell mateix. Aquest problema, en la literatura de components d'interfície, es coneix a vegades com prop drilling (perforació de propietats), i dues alternatives habituals ho eviten:

  • Un bus d'esdeveniments global: un objecte compartit, accessible des de qualsevol component de l'aplicació (normalment importat com un mòdul de JavaScript), sobre el qual qualsevol component pot despatxar esdeveniments i qualsevol altre pot subscriure's, sense passar per cap jerarquia de components intermèdia. Resol el problema de comunicació entre components llunyans, però a costa de perdre la traçabilitat clara de "qui es comunica amb qui" que sí que ofereix l'arbre de components i els seus esdeveniments amb bubbles/composed.
  • @lit/context, l'API de context compartit del propi ecosistema de Lit, dissenyada específicament perquè un component ancestre publiqui un valor i qualsevol descendent, a qualsevol profunditat, pugui consumir-lo directament, sense que els nivells intermedis necessitin reenviar res manualment. És, en general, l'alternativa més elegant per a aquest tipus de problema dins de l'ecosistema de Lit, i s'estudiarà en detall al mòdul 7, "Directives i Funcionalitats Avançades de Plantilles".

Per a la mida actual de TaskFlow, amb una jerarquia de només tres nivells (<task-board><task-list><task-card>), el patró d'elevar l'estat a l'ancestre comú, amb esdeveniments i propietats reenviats manualment, és perfectament adequat i no necessita cap d'aquestes dues alternatives; convé conèixer-les per reconèixer, en un projecte real més gran, el moment en què sí que convé recórrer-hi.

  1. Tancament: cap al cicle de vida i les actualitzacions

Amb <task-board> ja coordinant <task-list> i, en el futur, <task-filter>, TaskFlow ja té tota l'estructura de comunicació que necessitava: esdeveniments que pugen explicant el que ha passat, propietats que baixen amb l'estat actualitzat, i un ancestre comú que fa d'intermediari quan dos components no tenen relació directa de pare i fill.

Errors Comuns i Consells

  • Fer que <task-filter> i <task-list> intentin comunicar-se directament: per exemple, guardant una referència a <task-list> dins de <task-filter> amb document.querySelector i cridant els seus mètodes directament. Això trenca l'encapsulació d'ambdós components (cadascun passa a dependre de l'existència i de l'API interna de l'altre) i és exactament el que el patró d'aquesta lliçó evita.
  • Oblidar incloure al detail de l'esdeveniment reenviat la informació que es perdria en pujar un nivell: com es va explicar a l'apartat 4, <task-list> va haver d'afegir explícitament idTarea al detail de l'esdeveniment reenviat, perquè aquesta informació, disponible a <task-list> gràcies al tancament del map, no seria accessible d'una altra manera per a <task-board>.
  • Duplicar l'estat en diversos nivells de la jerarquia: si tant <task-board> com <task-list> mantinguessin la seva pròpia còpia de l'array tareas, sincronitzar-les correctament es tornaria innecessàriament complicat i propens a errors; l'estat compartit ha de viure en un únic lloc (l'ancestre comú), i els descendents s'han de limitar a rebre'l com a propietat, mai a mantenir la seva pròpia còpia independent.
  • Recórrer a un bus d'esdeveniments global o a @lit/context abans de necessitar-ho realment: per a jerarquies petites, com la TaskFlow d'aquest mòdul, el patró d'elevar l'estat a un ancestre comú és més senzill de seguir i depurar que introduir una capa addicional d'indirecció; convé reservar aquestes alternatives, esmentades a l'apartat 6, per quan la jerarquia o el nombre de components que comparteixen una dada ho justifiquin de veritat.

Exercicis

  1. Afegeix a <task-board> un mètode contarTareasPendientes() que retorni quantes tasques de this.tareas tenen estado === 'pendiente', i mostra-ho a la plantilla de <task-board>, al costat de l'<h1>TaskFlow</h1>, com un petit resum (per exemple, "3 tareas pendientes"). Explica per què aquesta lògica encaixa millor a <task-board> que a <task-list>.
  2. Suposa que s'afegeix tarea-eliminada (de l'exercici 1 de la lliçó 05-02) al flux complet de TaskFlow. Escriu el reenviament corresponent a <task-list> (similar a reenviarTareaCambiada) i el manejador corresponent a <task-board> que elimini la tasca de this.tareas de forma immutable.
  3. Explica, amb les teves pròpies paraules i recolzant-te en l'apartat 2, per què hauria estat un error resoldre el problema d'aquesta lliçó fent que <task-card> despatxés tarea-cambiada directament contra una referència guardada de <task-list> (per exemple, passant-li aquesta referència com a propietat des de <task-board>), en lloc de deixar que l'esdeveniment faci bombolla de forma natural fins on correspongui escoltar-lo.

Solucions

contarTareasPendientes() {
  return this.tareas.filter((tarea) => tarea.estado === 'pendiente').length;
}

render() {
  return html`
    <div class="tablero">
      <h1>TaskFlow</h1>
      <p>${this.contarTareasPendientes()} tareas pendientes</p>
      <task-list .tareas="${this.tareas}" @tarea-cambiada="${this.gestionarTareaCambiada}"></task-list>
    </div>
  `;
}

Aquesta lògica encaixa a <task-board> perquè necessita accés a l'array tareas complet, que és precisament la dada que <task-board> manté com a ancestre comú; <task-list>, si tingués aquesta mateixa lògica, necessitaria duplicar el criteri de comptatge i aplicar-lo també sobre la seva pròpia còpia de tareas, quan en realitat tots dos haurien d'operar sempre sobre la mateixa font de veritat, la que viu a <task-board>.

// task-list.js
reenviarTareaEliminada(idTarea) {
  this.dispatchEvent(
    new CustomEvent('tarea-eliminada', {
      detail: { idTarea },
      bubbles: true,
      composed: true,
    })
  );
}
// task-board.js
gestionarTareaEliminada(event) {
  const { idTarea } = event.detail;
  this.tareas = this.tareas.filter((tarea) => tarea.id !== idTarea);
}
<task-list
  .tareas="${this.tareas}"
  @tarea-cambiada="${this.gestionarTareaCambiada}"
  @tarea-eliminada="${this.gestionarTareaEliminada}"
></task-list>
  1. Passar una referència directa a <task-list> com a propietat de <task-card> (o de <task-board> cap a <task-card>, saltant-se <task-list>) obligaria <task-card> a conèixer, encara que fos indirectament, l'existència i l'API de <task-list>, exactament la dependència que la lliçó 05-02 buscava evitar: <task-card> deixaria de ser un component aïllat i reutilitzable, i passaria a dependre d'una peça concreta de la jerarquia de TaskFlow. A més, aquesta referència directa se saltaria completament el mecanisme de bombolla amb bubbles/composed, perdent l'avantatge assenyalat a la lliçó 05-02 de que qualsevol nombre de listeners, a qualsevol nivell de la jerarquia, pot escoltar el mateix esdeveniment sense que l'emissor necessiti saber res sobre ells; amb una referència directa a un únic destinatari, aquesta flexibilitat desapareix.

Conclusió

En aquesta lliçó s'ha resolt el problema de comunicar dos components sense relació directa de pare i fill mitjançant el patró d'elevar l'estat a un ancestre comú, aplicant dues vegades consecutives el cicle ja conegut d'esdeveniment cap amunt i propietat cap avall. <task-board> ha nascut com aquest ancestre comú, amb l'array tareas com a font única de veritat, <task-list> reenviant esdeveniments en lloc de gestionar-los per si sola, i el terreny ja preparat perquè <task-filter> s'incorpori més endavant com a germà de <task-list> sense que cap dels dos necessiti conèixer l'altre. També s'han esmentat, sense entrar en detall, dues alternatives per a jerarquies més grans: un bus d'esdeveniments global i @lit/context, aquesta última una alternativa més elegant que s'estudiarà amb detall al mòdul 7.

Tot aquest intercanvi d'esdeveniments i propietats que s'ha construït al llarg del mòdul 5 té un efecte comú, encara no explicat en profunditat: cada vegada que una propietat reactiva canvia —ja sigui estado a <task-card> o tareas a <task-board>— Lit programa una actualització, però fins ara aquest curs ha donat per fet, sense aturar-se a explicar-ho, quan exactament passa aquesta actualització, en quin ordre s'executen els diferents passos del procés, i quines possibilitats ofereix Lit per enganxar-se a moments concrets d'aquest cicle. Aquest és exactament el contingut del mòdul 6, "Cicle de Vida i Comportament Avançat".

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