La lliçó anterior va deixar <task-card> anunciant, mitjançant l'esdeveniment personalitzat tarea-cambiada, que l'usuari ha triat un nou estat per a una tasca. Però un anunci sense ningú que l'escolti no serveix de gaire: aquesta lliçó tanca el cicle complet, fent que <task-list> rebi aquest esdeveniment, actualitzi el seu propi array tareas i faci que aquest array actualitzat torni a baixar, com a propietat, cap a cada <task-card>. Pel camí apareix un concepte que, encara que no és exclusiu de Lit, resulta decisiu perquè la reactivitat funcioni bé: la immutabilitat de les dades que es guarden en una propietat reactiva.

Contingut

  1. Recordatori: de pare a fill, propietats
  2. Escoltant un esdeveniment personalitzat amb @evento
  3. Actualitzant l'array de tasques: el problema de mutar en el lloc
  4. La solució immutable: crear un array i un objecte nous
  5. Per què la immutabilitat importa per a la detecció de canvis de Lit
  6. El cicle complet a <task-list>
  7. Tancament: cap als components germans

  1. Recordatori: de pare a fill, propietats

El mòdul 3 ja va establir la via normal per la qual un component pare lliura dades a un component fill: propietats reactives, assignades des de la plantilla del pare amb el binding de punt (.propiedad="${valor}") quan la dada no és una simple cadena de text. És exactament el que <task-list> ja fa des del mòdul 2 en renderitzar cada targeta:

${this.tareas.map(
  (tarea) => html`
    <task-card
      .titulo="${tarea.titulo}"
      .estado="${tarea.estado}"
      .prioridad="${tarea.prioridad}"
      .urgente="${tarea.urgente}"
    ></task-card>
  `
)}

No hi ha res de nou a explicar aquí sobre el mecanisme en si mateix: continua essent el mateix binding de propietat de sempre. El que sí que és nou en aquesta lliçó és d'on surt el valor actualitzat de tarea.estado que es torna a passar en cada renderitzat: fins ara, this.tareas a <task-list> era un array fix, inicialitzat una sola vegada al constructor i mai modificat després. A partir d'aquesta lliçó, aquest array canvia de veritat, com a resposta al que passa dins de cada <task-card>.

  1. Escoltant un esdeveniment personalitzat amb @evento

La lliçó 05-01 va presentar @evento per a esdeveniments natius del navegador (@click, @keydown); la bona notícia és que la mateixa sintaxi, sense cap canvi, funciona exactament igual amb esdeveniments personalitzats com tarea-cambiada, perquè, com es va explicar a la lliçó anterior, un CustomEvent és, a tots els efectes del sistema d'esdeveniments del navegador, un Event més:

render() {
  return html`
    <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.gestionarTareaCambiada(tarea.id, event)}"
          ></task-card>
        `
      )}
    </div>
  `;
}

Aquí apareix un detall de disseny que mereix explicació: el manejador no és directament this.gestionarTareaCambiada (com als exemples de la lliçó 05-01), sinó una arrow function en línia, (event) => this.gestionarTareaCambiada(tarea.id, event). La raó és que gestionarTareaCambiada necessita saber a quina tasca concreta afecta el canvi, i aquesta informació —l'id de la tasca— només està disponible dins del cos de l'Array.map, a la variable tarea d'aquesta iteració concreta; el propi esdeveniment tarea-cambiada, tal com es va despatxar a la lliçó anterior, només porta en el seu detail el nou estat, no cap identificador de la tasca (perquè <task-card>, deliberadament, no coneix el concepte d'"id" ni l'estructura de l'array de <task-list>, com es va explicar a l'apartat 1 de la lliçó anterior). L'arrow function en línia "captura" el valor de tarea.id d'aquesta iteració concreta i l'afegeix com a argument addicional en cridar gestionarTareaCambiada, resolent així, en el costat de <task-list> (que sí que coneix la seva pròpia estructura de dades), la correspondència entre "quina targeta ha emès l'esdeveniment" i "quina tasca de l'array li correspon".

Com ja s'advertia a la lliçó 05-01 a propòsit de les arrow functions en línia dins de plantilles, això té el petit cost que Lit no pot reutilitzar exactament el mateix listener entre renderitzats successius (cada crida a render() crea una arrow function nova). Per al volum de targetes habitual en una llista de tasques, aquest cost és completament insignificant, i la claredat de poder capturar tarea.id directament en l'expressió compensa amb escreix l'alternativa d'intentar evitar-ho.

  1. Actualitzant l'array de tasques: el problema de mutar en el lloc

Amb l'esdeveniment ja arribant a gestionarTareaCambiada(idTarea, event), la temptació més directa és localitzar la tasca corresponent dins de this.tareas i modificar-la en el lloc:

// Incorrecte: muta l'array i l'objecte existents en el lloc
gestionarTareaCambiada(idTarea, event) {
  const tarea = this.tareas.find((t) => t.id === idTarea);
  tarea.estado = event.detail.nuevoEstado; // muta l'objecte existent
  this.requestUpdate(); // caldria forçar-ho manualment
}

Aquest codi, a primera vista, sembla raonable, i de fet "funciona" en el sentit que l'objecte tarea dins de l'array canvia la seva propietat estado correctament en memòria. El problema no és en aquesta mutació en si mateixa, sinó en com Lit decideix si una propietat reactiva ha canviat: com es va explicar al mòdul 3, canviar this.tareas dispara una actualització només si Lit detecta que el valor de la propietat és diferent de l'anterior. I aquí està el parany: this.tareas continua essent literalment el mateix array (la mateixa referència en memòria) abans i després d'aquesta mutació; només ha canviat un objecte dins d'ell. Lit, per a propietats de tipus objecte o array, compara referències, no contingut profund, així que no detecta cap canvi en absolut, i cap actualització es dispara de forma automàtica (d'aquí el this.requestUpdate() forçat a l'exemple anterior, un pedaç que amaga el problema real en lloc de resoldre'l correctament).

  1. La solució immutable: crear un array i un objecte nous

La forma correcta, i la que s'utilitza a partir d'ara a TaskFlow, és no mutar mai l'array ni els objectes que conté, sinó crear una còpia nova amb el canvi ja incorporat:

gestionarTareaCambiada(idTarea, event) {
  const nuevoEstado = event.detail.nuevoEstado;

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

Aquesta versió utilitza dues tècniques de JavaScript estàndard, sense res específic de Lit: Array.prototype.map(), que sempre retorna un array nou (mai modifica l'original), i l'operador de propagació d'objectes ({ ...tarea, estado: nuevoEstado }), que crea un objecte nou copiant totes les propietats de tarea i sobreescrivint, a continuació, només estado amb el valor rebut a l'esdeveniment. Per a les tasques que no coincideixen amb idTarea, map retorna la mateixa referència d'objecte que ja tenien (no cal copiar-les, perquè no han canviat); només la tasca afectada obté un objecte nou. El resultat final, this.tareas = ..., assigna a la propietat reactiva una referència d'array completament diferent a la que tenia abans, encara que el contingut de la majoria dels seus elements sigui el mateix objecte de sempre.

  1. Per què la immutabilitat importa per a la detecció de canvis de Lit

Amb la nova assignació, this.tareas = this.tareas.map(...), el setter generat per Lit per a la propietat tareas (el mateix mecanisme vist al mòdul 3 per a qualsevol propietat reactiva) compara la referència anterior de this.tareas amb la nova: són, literalment, dos arrays diferents en memòria, així que la comparació per defecte de Lit (!==, la comparació d'igualtat estricta) detecta la diferència sense cap ambigüitat, i programa una actualització exactament com amb qualsevol altra propietat.

Operació sobre this.tareas Canvia la referència? Lit detecta el canvi?
this.tareas[0].estado = 'hecha' (mutació directa d'un objecte intern) No No
this.tareas.push(nuevaTarea) (mutació directa de l'array) No No
this.tareas.sort(...) (mutació directa de l'array, encara que reordeni) No No
this.tareas = this.tareas.map(...)
this.tareas = [...this.tareas, nuevaTarea]
this.tareas = this.tareas.filter(...)

Aquesta taula resumeix una regla general que va més enllà d'aquest exemple concret i que convé interioritzar per a qualsevol propietat reactiva de tipus objecte o array a Lit: els mètodes d'array que modifiquen en el lloc (push, pop, splice, sort, reverse, o l'assignació directa a un índex o a una propietat anidada) mai canvien la referència de l'array o objecte contenidor, així que mai disparen una actualització per si sols; els mètodes que retornen una còpia nova (map, filter, concat, l'operador de propagació [...array] o {...objeto}) sí que canvien la referència, i són els que cal usar sempre que es vulgui que Lit reaccioni al canvi. No es tracta d'una limitació arbitrària de Lit: comparar el contingut profund d'un array en cada actualització (en lloc de només la seva referència) seria molt més costós computacionalment per a arbres de dades grans, així que Lit, igual que altres moltes biblioteques reactives, opta deliberadament per la comparació de referència, ràpida i predictible, a canvi d'exigir aquesta disciplina d'immutabilitat en el codi que la utilitza.

  1. El cicle complet a <task-list>

Amb totes les peces explicades, el codi complet de <task-list> amb el nou manejador queda així:

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

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

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

  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 },
    ];
  }

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

  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.gestionarTareaCambiada(tarea.id, event)}"
              ></task-card>
            `
          )}
        </div>
      </section>
    `;
  }
}

customElements.define('task-list', TaskList);

El recorregut complet, d'un extrem a l'altre, és ara el següent: l'usuari canvia el <select> d'una <task-card> concreta; <task-card> actualitza la seva pròpia propietat estado (per reflectir el canvi visualment de manera immediata, sense dependre de rebre de tornada la confirmació del seu pare) i despatxa tarea-cambiada amb el nou estat a detail; <task-list> rep l'esdeveniment a través de @tarea-cambiada, identifica gràcies al tancament sobre tarea.id a quina tasca de l'array correspon, i substitueix this.tareas per un array nou, amb un objecte nou per a aquesta tasca concreta i les altres intactes; Lit detecta el canvi de referència a this.tareas i torna a executar render(); l'Array.map de la plantilla itera de nou sobre l'array actualitzat i passa, mitjançant .estado="${tarea.estado}", el nou valor cap a cada <task-card> (inclosa la que va originar el canvi, que rep de tornada, com a propietat, exactament el mateix valor que ja tenia per la seva pròpia actualització interna). El cicle es tanca: un esdeveniment va pujar, una propietat va baixar.

  1. Tancament: cap als components germans

Val la pena notar que aquest flux —el fill emet un esdeveniment, el pare escolta i actualitza l'estat, l'estat actualitzat baixa de nou com a propietat— és exactament el mateix patró, sense cap variació conceptual, que es repetirà en la resta del curs cada vegada que un component necessiti comunicar un canvi cap amunt: el nom de l'esdeveniment canviarà, el detail transportarà dades diferents, la lògica d'actualització de l'array serà diferent, però l'estructura de fons (esdeveniment personalitzat cap amunt, propietat reactiva cap avall, immutabilitat pel mig) és sempre la mateixa.

Queda, però, un escenari que aquest patró, tal com està plantejat fins ara, no resol directament: què passa quan dos components que necessiten comunicar-se no tenen una relació directa de pare i fill, com passarà ben aviat entre <task-list> i un futur <task-filter>? Aquest és exactament el tema de la lliçó següent.

Errors Comuns i Consells

  • Mutar l'array o els objectes en el lloc i esperar que Lit reaccioni: com es va explicar a l'apartat 3, this.tareas[0].estado = 'hecha' o this.tareas.push(...) no canvien la referència de this.tareas, així que Lit no detecta cap canvi i la interfície no s'actualitza, encara que les dades en memòria sí que hagin canviat.
  • Utilitzar this.requestUpdate() com a pedaç en lloc de corregir la mutació: és possible forçar una actualització manual amb this.requestUpdate() després d'una mutació directa, i "funciona" en el sentit que la interfície es refresca; però és un símptoma, no una solució, d'estar mutant dades que s'haurien de tractar de forma immutable, i sol acumular deute tècnic difícil de rastrejar més endavant.
  • Copiar només el nivell més extern quan el canvi és més endins: si tarea tingués, per exemple, un objecte anidat (tarea.metadatos.ultimaModificacion), copiar només { ...tarea } no seria suficient perquè un canvi dins de metadatos es reflectís de forma immutable; caldria també { ...tarea, metadatos: { ...tarea.metadatos, ultimaModificacion: ahora } }, copiant cada nivell d'anidament que efectivament canviï.
  • Oblidar declarar tareas amb type: Array: sense aquesta declaració explícita a static properties, Lit no sabria que tareas és una propietat reactiva en absolut, i cap assignació a this.tareas, mutada o no, dispararia mai una actualització.

Exercicis

  1. Afegeix a <task-list> un mètode gestionarTareaEliminada(idTarea), enllaçat a l'esdeveniment tarea-eliminada de l'exercici 1 de la lliçó anterior, que elimini de this.tareas la tasca amb aquest id utilitzant Array.prototype.filter (de forma immutable, sense usar splice).
  2. Explica, basant-te en l'apartat 5, per què this.tareas.sort((a, b) => a.prioridad - b.prioridad) no provocaria cap actualització visible a <task-list>, i reescriu aquesta línia de manera que sí que la provoqui.
  3. Un company proposa simplificar gestionarTareaCambiada substituint l'Array.map per this.tareas = [...this.tareas]; this.tareas.find((t) => t.id === idTarea).estado = nuevoEstado;. Explica per què aquesta variant, encara que canvia la referència de l'array, continua sense ser una solució correctament immutable, i quin problema concret podria causar més endavant.

Solucions

gestionarTareaEliminada(idTarea) {
  this.tareas = this.tareas.filter((tarea) => tarea.id !== idTarea);
}
@tarea-eliminada="${() => this.gestionarTareaEliminada(tarea.id)}"

filter retorna sempre un array nou amb els elements que compleixen la condició, deixant fora la tasca eliminada, sense mutar en cap moment l'array original.

  1. sort és un dels mètodes d'array que muten l'array en el lloc i, a més, el retornen a si mateix com a valor de retorn; this.tareas continua essent, després de la crida, exactament la mateixa referència d'array que abans (només reordenada internament), així que el setter de Lit no detecta cap canvi i no dispara cap actualització, encara que l'ordre intern de l'array sí que hagi canviat en memòria. La forma correcta és copiar primer l'array i ordenar la còpia: this.tareas = [...this.tareas].sort((a, b) => a.prioridad - b.prioridad);. Aquí [...this.tareas] crea primer un array nou (trencant la referència anterior), i és sobre aquesta còpia nova sobre la qual sort muta en el lloc; com que l'assignació final a this.tareas sí que apunta a aquesta referència nova, Lit detecta el canvi correctament.

  2. Encara que [...this.tareas] crea un array nou (pel qual Lit sí que detectaria el canvi de referència i dispararia una actualització), l'operador de propagació d'un array només copia el propi array, no els objectes que conté: els elements de la còpia continuen essent les mateixes referències d'objecte que a l'array original. Per tant, .find(...).estado = nuevoEstado continua mutant en el lloc el mateix objecte tarea que també està referenciat des de l'array anterior (si en algun altre lloc del codi s'hagués guardat una referència a aquest array previ, per exemple per comparar "abans i després", aquest objecte també apareixeria ja modificat, encara que en teoria representés l'estat "antic"). El problema concret és subtil però real: qualsevol codi que depengués de que els objectes de l'array anterior romanguessin sense modificar (una cosa habitual, per exemple, en eines de depuració amb historial d'estats, o en comparacions superficials d'objectes) es trencaria silenciosament. La solució correcta, com es va explicar a l'apartat 4, és crear també un objecte nou per a l'element afectat amb { ...tarea, estado: nuevoEstado }, no només un array nou.

Conclusió

Amb aquesta lliçó es completa el cicle real de comunicació entre <task-card> i <task-list>: un esdeveniment personalitzat puja des de la targeta que detecta la interacció de l'usuari, i una propietat actualitzada baixa des de la llista que decideix com ha de canviar l'estat compartit. La peça que ho fa possible, més enllà de la sintaxi de @evento i CustomEvent ja vistes, és la immutabilitat: mai mutar directament un array o un objecte que viu en una propietat reactiva, sinó reemplaçar-lo sempre per una còpia nova amb el canvi incorporat, perquè la comparació per referència de Lit pugui detectar que alguna cosa ha canviat.

Aquest patró resol la comunicació entre un pare i el seu fill directe, però TaskFlow està a punt de créixer amb components que no tenen aquesta relació tan senzilla: a la lliçó següent, "Patrons de Comunicació entre Components Germans", s'abordarà què fer quan dos components que necessiten compartir informació —com la futura parella <task-list> i <task-filter>— no són pare i fill directe, sinó germans sota un mateix contenidor.

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