La lliçó anterior acabava amb un compromís: veure la mateixa pantalla de Nómada Tasques —la llista de tasques amb el seu filtre per responsable i el seu botó de marcar com a feta— escrita de quatre maneres diferents, perquè la comparació sigui real i no un catàleg de característiques. Comencem per React, que és el més estès i també el més fàcil de malentendre, perquè la seva superfície és enganyosament petita: un grapat de funcions i una sintaxi estranya. El difícil de React no és aprendre'l, és entendre quan es torna a executar el teu codi i per què. Aquesta lliçó va exactament d'això. Veuràs què és JSX i en què es converteix en compilar; com s'escriuen components de funció, com es passen props i com es componen; per què les llistes necessiten una key —i tancaràs per fi el paral·lelisme amb el teu reconciliar() per data-id de 06-06—; com funciona useState i per què l'estat s'ha de tractar com a immutable, on lluiran les actualitzacions immutables de 04-07; en què es diferencien els esdeveniments de React del teu addEventListener; què és useEffect, i sobretot per a què no serveix, amb el seu array de dependències, la seva funció de neteja —que és el teu destruir() de 09-03— i l'error clàssic de fer-lo servir per derivar estat; què fan useMemo, useCallback i useRef amb l'advertiment de 09-01 sobre no optimitzar sense mesurar; com s'extreu lògica reutilitzable en hooks personalitzats, construint useTauler i useFiltreResponsable; la diferència entre formularis controlats i no controlats; com funciona de debò el DOM virtual; i com es carreguen dades amb els seus tres problemes reals —condicions de cursa, cancel·lació i doble execució—. Al final, la llista de tasques completa en React, comparada línia a línia amb el teu tauler-vista.js.

Contingut

  1. Què és React i què no és
  2. Posar en marxa un projecte
  3. JSX: què és i en què es converteix
  4. Components de funció
  5. props i composició
  6. Renderitzat de llistes i la key
  7. key: el mateix problema que vas resoldre a 06-06
  8. Renderitzat condicional
  9. Estat amb useState
  10. Per què l'estat es tracta com a immutable
  11. La forma de l'estat importa més que l'estat
  12. Esdeveniments a React i la diferència amb addEventListener
  13. useEffect: què és i què no és
  14. L'array de dependències
  15. La funció de neteja: el teu destruir()
  16. L'error de derivar estat amb efectes
  17. useMemo, useCallback i useRef
  18. No optimitzis sense mesurar
  19. Hooks personalitzats: useTauler i useFiltreResponsable
  20. Les regles dels hooks i per què existeixen
  21. Formularis controlats i no controlats
  22. El DOM virtual i la reconciliació, de debò
  23. El compilador de React i els components de servidor
  24. Carregar dades amb useEffect i els seus tres problemes
  25. Per què en producció es fa servir una llibreria de dades
  26. L'ecosistema mínim
  27. Nómada Tasques en React: la llista completa
  28. Comparació costat a costat amb tauler-vista.js
  29. Errors Habituals i Consells
  30. Exercicis
  31. Conclusió

  1. Què és React i què no és

React és una llibreria per construir interfícies d'usuari. La paraula «llibreria» és deliberada i la distinció de 10-01 s'aplica al peu de la lletra: React resol un problema —descriure la pantalla en funció de l'estat i mantenir-la sincronitzada— i no en resol cap més.

El que React et dona:

  • Un model de components de funció.
  • Un sistema d'estat local (useState) i d'efectes (useEffect).
  • Un motor de reconciliació que tradueix les teves descripcions en operacions mínimes sobre el DOM.
  • Un model d'esdeveniments uniforme entre navegadors.

El que React no et dona i has de triar tu:

Necessitat React inclou Què es fa servir a la pràctica
Encaminament Res React Router, TanStack Router, o el del meta-framework
Peticions HTTP Res fetch (el teu demanarJson de 07-03), TanStack Query
Estat global Context, que és un mecanisme de transport, no un magatzem Redux Toolkit, Zustand, Jotai (10-03)
Formularis Res més enllà de l'estat React Hook Form, o a mà
Estils Res CSS normal, mòduls CSS, utilitats, CSS-in-JS
Empaquetament Res Vite (el que ja coneixes de 09-05)
Proves Res Jest o Vitest + Testing Library (08-03, 08-05)

Aquesta taula és la definició operativa de «llibreria sense opinions». Té una conseqüència pràctica que veuràs tan bon punt miris codi aliè: dos projectes React poden no assemblar-se gens. També té un avantatge evident: el teu demanarJson amb reintents i AbortController de 07-03 es pot fer servir tal qual, sense cap adaptador, perquè React no té opinió sobre com demanes dades.

Un punt que convé fixar des del principi: React no és un llenguatge. Tot el que escriuràs és JavaScript, amb una única extensió de sintaxi (JSX) que es compila a crides de funció normals. Els map, filter i reduce de 04-05, la desestructuració de 04-07, el spread immutable, les promeses de 05-06 i els mòduls de 05-04 es fan servir exactament igual. Aquesta és una de les raons de la seva popularitat: gairebé tot el que saps es transfereix.

  1. Posar en marxa un projecte

Amb Vite, que ja fas servir des de 09-05, un projecte React nou es crea així:

npm create vite@latest nomada-react -- --template react
cd nomada-react
npm install
npm run dev

El que s'instal·la són dos paquets: react (el motor de components, independent de la plataforma) i react-dom (el que sap pintar en un navegador). Aquesta separació existeix perquè el mateix motor pinta en mòbil natiu amb React Native.

El punt d'entrada és mínim:

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './estils.css';

createRoot(document.getElementById('arrel')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Tres coses per llegir amb atenció:

  • createRoot(node) pren un element del DOM real —el mateix <div id="arrel"> de sempre— i el converteix en el territori de React. Fora d'aquell node, React no toca res. Pots muntar React en un racó d'una pàgina existent; és el que fa molta gent quan migra.
  • .render(<App />) li diu què ha de pintar a dins.
  • <StrictMode> és un embolcall de desenvolupament que executa coses dues vegades a propòsit per destapar efectes mal escrits. No fa res en producció. L'odiaràs a l'apartat 24 i després l'agrairàs.

  1. JSX: què és i en què es converteix

Això és JSX:

const element = <h2 className="tasca__titol">Pressupost de la fusteria</h2>;

No és HTML dins de JavaScript, ni una plantilla de text. És sucre sintàctic que el compilador (esbuild dins de Vite, o Babel) converteix en una crida de funció. Això anterior es converteix, aproximadament, en això:

// El que produeix el compilador (simplificat)
import { jsx } from 'react/jsx-runtime';

const element = jsx('h2', {
  className: 'tasca__titol',
  children: 'Pressupost de la fusteria'
});

I el que retorna aquesta crida és un objecte JavaScript corrent, no un node del DOM:

{
  type: 'h2',
  props: { className: 'tasca__titol', children: 'Pressupost de la fusteria' },
  key: null
}

Aquest objecte és l'element de React, el maó del DOM virtual del qual parlava 10-01. És lleuger, és de només lectura i no ha tocat el DOM. Compara'l amb el que feia el teu crearElement de js/vista/dom.js: allò creava un node real immediatament; això en descriu un i decideix després.

Entendre que JSX són crides de funció explica totes les seves regles, que d'una altra manera semblen arbitràries:

Regla 1 · Un component retorna un sol element arrel. Perquè return retorna un valor, no dos. Quan no vols un contenidor extra, es fa servir el fragment <>…</>:

return (
  <>
    <h2>{tasca.titol}</h2>
    <p>{tasca.responsable}</p>
  </>
);

Regla 2 · Les claus contenen una expressió JavaScript, no una sentència. {tasca.titol}, {hores * 2}, {esVencuda ? '⚠' : ''} funcionen; un if o un for no, perquè no produeixen valor. Per això les llistes es fan amb map i els condicionals amb l'operador ternari o amb &&.

Regla 3 · Els atributs fan servir els noms de les propietats del DOM, no els de l'HTML. class és paraula reservada a JavaScript, així que s'escriu className; el for de les etiquetes és htmlFor; els gestors són onClick, onInput, en camelCase. Els atributs data-* i aria-* són l'excepció i s'escriuen tal qual, amb guions, perquè no són propietats reals.

<li className="tasca tasca--alta" data-id={6} aria-label="Tasca vençuda">
  <label htmlFor="filtre">Responsable</label>
</li>

Regla 4 · {} interpola valors, i alguns valors no es pinten. null, undefined, false i true no produeixen res. Això és el que fa possible el renderitzat condicional de l'apartat 8, i també la causa d'un error clàssic: {tasques.length && <Llista/>} pinta un 0 a la pantalla quan la llista és buida, perquè 0 que es pinta.

Regla 5 · El contingut interpolat s'escapa. {tasca.titol} s'insereix com a text, mai com a HTML. És l'equivalent del teu textContent davant d'innerHTML de 06-02, i protegeix contra la injecció de la mateixa manera. Per inserir HTML de debò cal fer servir una propietat amb un nom deliberadament incòmode (dangerouslySetInnerHTML), que és exactament l'avís que vols tenir.

  1. Components de funció

Un component de React és una funció que rep un objecte de propietats i retorna una descripció de pantalla. Res més:

// src/components/TargetaTasca.jsx
export function TargetaTasca({ tasca }) {
  return (
    <li className="tasca">
      <h3 className="tasca__titol">{tasca.titol}</h3>
      <p className="tasca__meta">
        {tasca.responsable ?? 'sense assignar'} · {tasca.horesEstimades} h
      </p>
    </li>
  );
}

Dues convencions que no són negociables:

  1. El nom comença per majúscula. No és estil: és com el compilador distingeix <TargetaTasca/> (el teu component) de <li> (una etiqueta HTML). En minúscula, JSX genera la cadena 'targetatasca' i React intentarà crear un element HTML inexistent.
  2. El component ha de ser pur respecte a les seves entrades. Amb les mateixes props i el mateix estat, la mateixa sortida. No modifica les seves props, no escriu en variables externes durant el renderitzat, no toca el DOM directament. Aquesta és la f d'UI = f(estat), i tot el model depèn que es compleixi.

Fixa't en la desestructuració de la signatura: function TargetaTasca({ tasca }). És la desestructuració d'objectes en paràmetres de 04-07, i és l'estil dominant perquè documenta quines props rep el component només llegint-ne la primera línia.

  1. props i composició

Les props són les dades que un component rep del seu pare. Flueixen cap avall i són de només lectura: un component no ha de modificar l'objecte que rep.

// El pare decideix quines dades i quin comportament rep el fill
<TargetaTasca tasca={tasca} enMarcarFeta={marcarFeta} />

Es passen tres tipus de coses, i convé distingir-les:

Tipus de prop Exemple Per a què
Dades tasca={tasca}, hores={45} El que cal pintar
Funcions enMarcarFeta={marcarFeta} Com avisar cap amunt que ha passat alguna cosa
Contingut children Composició: què va a dins

Les props de funció són el mecanisme de comunicació cap amunt, i són l'equivalent directe dels teus CustomEvent de 06-04, amb una diferència important: el CustomEvent viatja pel DOM i qualsevol el pot escoltar; la prop de funció és un contracte explícit entre pare i fill que es llegeix a la signatura.

La composició amb children és la peça que més s'infrautilitza:

// Un component contenidor genèric
export function Plafo({ titol, children }) {
  return (
    <section className="plafo">
      <h2 className="plafo__titol">{titol}</h2>
      <div className="plafo__cos">{children}</div>
    </section>
  );
}

// Ús: el que va a dins arriba com a children
<Plafo titol="Tasques obertes">
  <LlistaTasques tasques={visibles} />
</Plafo>

children no és màgia: és una prop més, que JSX omple amb el que hagis escrit entre l'etiqueta d'obertura i la de tancament. I dona un patró molt potent: components que defineixen el forat sense saber què hi va a dins, que és el que fan els <slot> dels web components i, com veuràs a 10-04, els de Vue.

  1. Renderitzat de llistes i la key

Una llista es pinta amb map, el mateix de 04-04:

export function LlistaTasques({ tasques }) {
  return (
    <ul className="llista-tasques">
      {tasques.map((tasca) => (
        <TargetaTasca key={tasca.id} tasca={tasca} />
      ))}
    </ul>
  );
}

tasques.map(...) retorna un array d'elements de React, i JSX sap pintar un array posant-ne els elements un darrere l'altre. Res de nou tret d'un detall: key.

  1. key: el mateix problema que vas resoldre a 06-06

Aquest és el moment de tancar el cercle que es va obrir a 06-06 i que 09-05 va deixar apuntat.

Quan l'estat canvia, React torna a executar LlistaTasques i obté un array nou de descripcions. Ara ha de decidir, per a cada element de l'array nou, quin de l'array vell li correspon. Sense més informació, l'única heurística disponible és la posició: el primer amb el primer, el segon amb el segon.

Aquesta heurística falla exactament en els mateixos casos que et van obligar a escriure reconciliar amb data-id. Suposa el backlog canònic i que s'elimina la tasca 2:

Posició Abans Després Què faria React sense key
0 Tasca 1 · Sala polivalent Tasca 1 · Sala polivalent Reutilitza el node. Correcte
1 Tasca 2 · Cartelleria Tasca 3 · Web de reserves Reutilitza el node de la 2 i li canvia el text
2 Tasca 3 · Web de reserves Tasca 4 · Inventari Reutilitza el node de la 3 i li canvia el text
3 Tasca 4 · Inventari Tasca 5 · Guia Reutilitza i canvia
4 Tasca 5 · Guia Tasca 6 · Fusteria Reutilitza i canvia
5 Tasca 6 · Fusteria Elimina l'últim node

Visualment el resultat és correcte. Però cinc nodes han canviat de dada, i amb ells:

  • El focus, que era al botó «Començar» de la tasca 3, acaba en un botó que ara mostra la tasca 4.
  • Qualsevol transició CSS en curs s'aplica a l'element equivocat.
  • L'estat intern dels components fills —un menú desplegat, un <input> a mig escriure— s'atribueix a la tasca que no era.
  • Es fan cinc actualitzacions de text quan n'hi havia prou d'eliminar un node.

És paraula per paraula l'anàlisi que vas fer a l'exercici de 06-06. La solució és la mateixa: una clau estable que identifiqui la dada, no la seva posició.

<TargetaTasca key={tasca.id} tasca={tasca} />

Amb key, React construeix un mapa de clau → element anterior —exactament el Map que construeix el teu reconciliar amb node.dataset.id— i aparella per clau en lloc de per posició. Reutilitza els que continuen vius, mou els que han canviat de lloc i elimina els sobrants.

Les tres regles de key, que són les mateixes que les del teu data-id:

Regla Per què
Estable: la mateixa dada té sempre la mateixa clau Si canvia, React destrueix i recrea: perds focus, estat i transicions
Única entre germans: no globalment Només es compara dins de la mateixa llista
Mai l'índex si la llista es reordena, filtra o admet eliminacions L'índex és la posició: fer-lo servir equival a no posar clau

Un matís sobre l'índex, perquè hi ha moltíssima desinformació: fer servir l'índex com a clau no sempre és un error. Si la llista no es reordena mai, no es filtra mai, no s'hi insereix ni s'hi elimina res pel mig i els seus elements no tenen estat propi, l'índex és exactament igual de bo que un id. El problema és que aquestes quatre condicions deixen de complir-se el dia que algú afegeix un filtre, i llavors la fallada és subtil, intermitent i difícil de reproduir. La regla pràctica: si tens un id, fes-lo servir.

I un avís que estalvia hores: key no és una prop. React la consumeix i el component no la rep. Si TargetaTasca necessita l'id, cal passar-l'hi també: <TargetaTasca key={t.id} tasca={t} /> funciona perquè l'id va dins de tasca.

  1. Renderitzat condicional

Tres maneres, amb criteris clars per triar:

// 1 · Operador ternari: quan hi ha dues alternatives
{tasca.estat === 'feta'
  ? <span className="marca">✓ Feta</span>
  : <button onClick={avancar}>Començar</button>}

// 2 · && : quan o es pinta alguna cosa o no es pinta res
{tasca.estaVencuda(AVUI) && <span className="avis">⚠ Vençuda</span>}

// 3 · Return anticipat: quan el component sencer canvia
export function LlistaTasques({ tasques }) {
  if (tasques.length === 0) {
    return <p className="buit">Cap tasca no coincideix amb el filtre.</p>;
  }
  return <ul className="llista-tasques">{/* … */}</ul>;
}

El parany del && mereix el seu propi exemple perquè hi cau tothom un cop:

{tasques.length && <Resum tasques={tasques} />}

Si tasques és buit, tasques.length val 0. L'operador && retorna 0, i 0 no és dels valors que React ignora: es pinta. Apareix un zero solt a la pantalla. La solució és convertir-lo en booleà de debò:

{tasques.length > 0 && <Resum tasques={tasques} />}

És un recordatori directe de 01-07: els valors falsy no són tots equivalents, i aquí la diferència entre 0 i false es veu literalment a la pantalla.

  1. Estat amb useState

Fins ara tot era estàtic. L'estat és el que fa que la pantalla canviï.

import { useState } from 'react';

export function FiltreResponsable({ responsables, valor, enCanviar }) {
  return (
    <label className="filtre">
      Responsable:
      <select value={valor ?? ''} onChange={(e) => enCanviar(e.target.value || null)}>
        <option value="">Tots</option>
        {responsables.map((nom) => (
          <option key={nom} value={nom}>{nom}</option>
        ))}
      </select>
    </label>
  );
}

Aquest component no té estat propi: el rep i avisa cap amunt. L'estat viu al pare:

export function App() {
  const [responsable, setResponsable] = useState(null);
  // …
}

useState(inicial) retorna un array de dues posicions que es desestructura (04-06): el valor actual i la funció per canviar-lo. Quatre punts essencials:

1 · El valor inicial només es fa servir en el primer renderitzat. Als següents, React ignora l'argument i retorna el valor guardat. Si el valor inicial és car de calcular, es passa una funció perquè només s'executi un cop:

const [tauler] = useState(() => crearTaulerDesDe(BACKLOG));  // es crida UNA vegada
const [taulerMalament] = useState(crearTaulerDesDe(BACKLOG)); // s'executa a CADA render

Aquesta distinció és exactament la diferència entre passar una funció i passar-ne el resultat, de 03-02. Amb un tauler de 600 tasques, la segona forma construeix 600 objectes a cada render i en llença 599.

2 · Cridar la funció d'actualització demana un nou renderitzat. No canvia la variable actual: responsable continua valent el mateix fins al final d'aquesta execució. És l'error de principiant número u:

function enPremer() {
  setResponsable('Iván');
  console.log(responsable);   // ← encara null: la variable NO canvia aquí
}

És un closure (03-04): responsable és una constant capturada en aquesta execució de la funció component. El valor nou arriba en la següent execució. Veure-ho així, i no com «React triga», elimina la confusió d'arrel.

3 · Les actualitzacions s'agrupen. Diversos set* seguits produeixen un sol renderitzat. I si el valor nou depèn de l'anterior, cal fer servir la forma de funció:

setComptador(comptador + 1);
setComptador(comptador + 1);   // ← tots dos llegeixen el MATEIX valor: incrementa 1, no 2

setComptador((n) => n + 1);
setComptador((n) => n + 1);    // ← cadascun rep el resultat de l'anterior: incrementa 2

4 · Si el valor nou és idèntic (Object.is) a l'anterior, React no torna a renderitzar. Aquí hi ha el motiu pel qual la immutabilitat no és opcional.

  1. Per què l'estat es tracta com a immutable

React compara el valor nou amb l'anterior fent servir Object.is, que per a objectes i arrays compara identitat de referència, no contingut. És la comparació de 04-08.

const tasques = [/* … */];
tasques[5].estat = 'feta';       // l'objecte canvia
setTasques(tasques);             // ← mateixa referència: React NO renderitza

La pantalla no s'actualitza encara que la dada hagi canviat. L'error no és de React: és que li has lliurat el mateix array de sempre i li has demanat que noti la diferència.

La solució és la que fas servir des de 04-07: crear valors nous en lloc de modificar els existents.

// Marcar la tasca 6 com a feta, de forma immutable
setTasques((actuals) =>
  actuals.map((t) => (t.id === 6 ? { ...t, estat: 'feta' } : t))
);

Llegeix-ho amb cura, perquè és el patró que més escriuràs a React:

  • map retorna un array nou: la referència canvia, React detecta el canvi.
  • Per a les tasques que no són la 6 retorna el mateix objecte: la referència es conserva. Això no és un detall, és una optimització clau — permet que React (i React.memo) sàpiga que aquelles targetes no han canviat.
  • Per a la 6, { ...t, estat: 'feta' } crea un objecte nou amb tot l'anterior i el camp canviat. És el spread de 04-07, exactament.

Aquí hi ha una tensió real amb Nómada Tasques que convé anomenar. La teva classe Tasca#estat privat i un mètode canviarEstat que muta l'objecte validant la R6. Això és bon disseny orientat a objectes i és incompatible amb la detecció per referència de React. Hi ha tres sortides honestes:

Opció Com Quan convé
Dades planes a l'estat de React, classes fora L'estat guarda objectes literals; les regles viuen en funcions pures que reben i retornen objectes El més comú i el més simple
Classes immutables canviarEstat retorna una instància nova en lloc de mutar Manté el model, requereix reescriure'l
El tauler fora de React, sincronitzat El Tauler continua sent teu i un comptador de versió força el renderitzat Poc recomanable: dues fonts de veritat

Per a la reimplementació de l'apartat 27 farem servir la primera, que és el que fa la majoria, i ho direm explícitament. És un exemple concret del que 10-01 anomenava «el cost de la dependència»: el framework té opinió sobre la forma de les teves dades.

  1. La forma de l'estat importa més que l'estat

Un consell que estalvia molt patiment i que s'aprèn tard: gairebé tots els problemes difícils d'estat a React són problemes d'estat mal modelat.

Tres regles pràctiques:

No guardis el que pots calcular. Això està malament:

const [tasques, setTasques] = useState(BACKLOG);
const [horesObertes, setHoresObertes] = useState(45);   // ← redundant i sincronitzable a mà

Si horesObertes es dedueix de tasques, guardar-ho crea dues fonts de veritat que cal mantenir a mà — el problema 1 de 10-01, reintroduït dins del framework. El correcte:

const [tasques, setTasques] = useState(BACKLOG);
const horesObertes = tasques
  .filter((t) => t.estat !== 'feta')
  .reduce((s, t) => s + t.horesEstimades, 0);   // es recalcula a cada render, i està bé

Sí: es recalcula a cada renderitzat. I sí, està bé. Sumar sis números, o sis-cents, és irrellevant comparat amb la feina de pintar. Només si mesures que fa mal es memoïtza (apartat 17).

Guarda identificadors, no objectes. Si tens tascaSeleccionada com a objecte i la tasca canvia a la llista, tens una còpia obsoleta. Guarda idSeleccionat i cerca la tasca en pintar.

Evita l'estat impossible. { carregant: true, error: 'fallada' } és un estat que no hauria d'existir. Modelar-lo com una sola variable amb valors 'inactiu' | 'carregant' | 'llest' | 'error' elimina la combinació impossible per construcció. És el mateix raonament que et va portar a SEGUENT[estat] a la R6.

  1. Esdeveniments a React i la diferència amb addEventListener

A React els gestors es declaren com a props:

<button onClick={() => enMarcarFeta(tasca.id)}>Marcar feta</button>

Sembla l'onclick d'HTML dels anys noranta, però funciona de manera completament diferent. Taula comparativa amb el que coneixes de 06-03 i 06-04:

Aspecte addEventListener (06-03) React
On es registra Al node concret React fa servir delegació a l'arrel de l'aplicació
Quantes escoltes hi ha Una per node Una per tipus d'esdeveniment, per a tota l'aplicació
Què rep el gestor L'Event natiu Un SyntheticEvent que normalitza diferències entre navegadors
Accés a l'esdeveniment natiu Directe e.nativeEvent
Cancel·lar el comportament e.preventDefault() o return false en alguns casos Només e.preventDefault()
Retirar l'escolta removeEventListener o signal Automàtic en desmuntar
Nom de l'esdeveniment d'escriptura input onChange (que es comporta com l'input natiu)

Dues conseqüències pràctiques:

React ja fa delegació per tu. El patró que vas construir a 06-04 amb closest('[data-accio]') per no posar 600 escoltes és exactament el que fa React internament. Escriure onClick a cadascuna de les 600 targetes no crea 600 escoltes del DOM: en crea una a l'arrel. És una de les coses que un framework et dona resolta de fàbrica i que tu vas resoldre a mà.

onChange no és el change natiu. És una de les poques incoherències històriques de React: onChange es dispara a cada pulsació, com l'input natiu, no en perdre el focus com el change del DOM. Si véns de JavaScript pur, és el parany que més desconcerta.

  1. useEffect: què és i què no és

useEffect és el hook pitjor entès de React, i la majoria dels problemes vénen d'una idea equivocada: creure que és «codi que s'executa quan alguna cosa canvia». No ho és.

useEffect serveix per sincronitzar el teu component amb un sistema extern a React.

«Sistema extern» significa: el DOM fora del teu arbre, un WebSocket, un setInterval, localStorage, el títol del document, una llibreria de tercers, l'observador d'intersecció. Coses que existeixen fora del model UI = f(estat) i que cal encendre i apagar.

import { useEffect } from 'react';

function TitolDocument({ pendents }) {
  useEffect(() => {
    document.title = `Nómada Tasques (${pendents})`;
  }, [pendents]);

  return null;
}

El document.title és un sistema extern: React no el gestiona. Sincronitzar-lo amb l'estat és exactament el cas d'ús.

I ara la llista de per a què NO serveix useEffect, que és més útil:

No el facis servir per… Fes servir en el seu lloc
Calcular un valor a partir de l'estat o les props Calcular-lo directament durant el renderitzat
Reaccionar a un clic de l'usuari El gestor de l'esdeveniment
Transformar dades abans de pintar-les Una expressió, o useMemo si mesures que fa mal
Actualitzar estat quan canvia una prop Repensar l'estat; gairebé sempre és estat derivat
Carregar dades en una aplicació de producció Una llibreria de dades (apartat 25)

La regla que ho resumeix tot: si l'efecte no té res per apagar i no toca res fora de React, probablement no hauria de ser un efecte.

  1. L'array de dependències

El segon argument d'useEffect controla quan es torna a executar:

useEffect(() => { /* … */ });               // a CADA renderitzat
useEffect(() => { /* … */ }, []);           // només en muntar
useEffect(() => { /* … */ }, [id, filtre]); // en muntar i quan canviï id o filtre

La comparació de les dependències es fa amb Object.is, la mateixa de l'apartat 10. I d'aquí en surt el problema més freqüent de tots:

function Llista({ tasques, filtres }) {
  useEffect(() => {
    console.log('els filtres han canviat');
  }, [filtres]);      // ← si el pare crea {responsable: null} a cada render, això es dispara SEMPRE
}

Un objecte literal escrit dins del component pare és un objecte nou a cada renderitzat. La seva referència canvia sempre, així que la dependència sempre sembla haver canviat. Les solucions, per ordre de preferència:

  1. Dependre de valors primitius: [filtres.responsable, filtres.text] en lloc de [filtres]. Gairebé sempre és el correcte.
  2. Moure la creació de l'objecte fora del component si és constant.
  3. Memoïtzar l'objecte amb useMemo al pare. És l'últim recurs, no el primer.

I una regla que no admet excepcions pràctiques: declara totes les dependències que l'efecte fa servir. La regla d'ESLint react-hooks/exhaustive-deps ho comprova, i —connectant amb 08-02— és una de les raons per les quals un projecte React sense ESLint és una mala idea. Quan sentis la temptació de silenciar-la, gairebé sempre significa que l'efecte està mal plantejat, no que la regla s'equivoqui.

  1. La funció de neteja: el teu destruir()

Si l'efecte retorna una funció, React la crida abans de la següent execució de l'efecte i en desmuntar el component. És el mecanisme de cicle de vida de 10-01, i és literalment el teu destruir() de 09-03.

Compara. El que vas escriure en JavaScript pur:

// js/vista/tauler-vista.js — 09-03
export class TaulerVista {
  #controlador = new AbortController();

  constructor(contenidor) {
    this.canal = new CanalTauler();
    this.canal.subscriure(this.#enRebre);
    window.addEventListener('resize', this.#enRedimensionar,
                            { signal: this.#controlador.signal });
  }

  destruir() {
    this.#controlador.abort();      // treu TOTES les escoltes registrades amb el senyal
    this.canal.tancar();
    clearInterval(this.#rellotge);
  }
}

I el mateix a React:

function TaulerEnViu({ enRebre }) {
  useEffect(() => {
    const canal = new CanalTauler();
    canal.subscriure(enRebre);

    const controlador = new AbortController();
    window.addEventListener('resize', enRedimensionar, { signal: controlador.signal });

    const rellotge = setInterval(() => recalcularVencudes(), 60_000);

    return () => {                  // ← això és destruir()
      canal.tancar();
      controlador.abort();
      clearInterval(rellotge);
    };
  }, [enRebre]);

  return null;
}

El contingut de la neteja és idèntic. El que canvia és qui la crida: en la teva versió, algú s'ha de recordar d'invocar destruir() des del lloc correcte en el moment correcte —i si reconciliar elimina un node, ningú no avisa—. A React, la crida està garantida. Aquesta és la millora exacta: no desapareix la feina, desapareix l'oblit.

Un detall que confon al principi: la neteja s'executa també entre execucions successives de l'efecte, no només en desmuntar. Si enRebre canvia, React primer neteja el canal vell i després en crea un de nou. És correcte i és el que vols: l'efecte descriu «mentre aquestes dependències valguin això, ha d'existir aquesta subscripció».

  1. L'error de derivar estat amb efectes

Aquest és l'antipatró més estès de React. Es veu així:

// ❌ MALAMENT: un efecte per calcular una cosa que es dedueix de l'estat
function Plafo({ tasques }) {
  const [horesObertes, setHoresObertes] = useState(0);

  useEffect(() => {
    setHoresObertes(
      tasques.filter((t) => t.estat !== 'feta')
             .reduce((s, t) => s + t.horesEstimades, 0)
    );
  }, [tasques]);

  return <p>{horesObertes} h obertes</p>;
}

Tres coses van malament, i totes són conseqüències, no opinions:

  1. Hi ha dos renderitzats per cada canvi. El primer pinta el valor vell; l'efecte s'executa i canvia l'estat; el segon pinta el correcte. L'usuari pot veure el número antic durant un fotograma.
  2. Hi ha dues fonts de veritat. tasques i horesObertes es poden desincronitzar: és el problema 1 de 10-01 reinventat dins del framework que venia a resoldre'l.
  3. És més codi i més lent.

La versió correcta cap en una línia:

// ✅ BÉ: es calcula durant el renderitzat
function Plafo({ tasques }) {
  const horesObertes = tasques
    .filter((t) => t.estat !== 'feta')
    .reduce((s, t) => s + t.horesEstimades, 0);

  return <p>{horesObertes} h obertes</p>;
}

La regla, memoritzable: si pots calcular-ho durant el renderitzat, calcula-ho durant el renderitzat. No és una microoptimització: és evitar reintroduir el problema que el model declaratiu venia a eliminar.

  1. useMemo, useCallback i useRef

Tres hooks que es confonen constantment. Taula primer:

Hook Què guarda entre renderitzats Quan es recalcula Per a què serveix de debò
useMemo(fn, deps) El resultat de fn() Quan canvia alguna dependència Evitar un càlcul car, o mantenir estable la referència d'un objecte
useCallback(fn, deps) La funció mateixa Quan canvia alguna dependència Mantenir estable la referència d'una funció que es passa com a prop o dependència
useRef(inicial) Un objecte { current } mutable Mai; persisteix sempre Guardar alguna cosa que no ha de provocar renderitzat, o accedir a un node real del DOM

useMemo és exactament la teva memòria cau per versió de 09-02, amb les dependències fent de comptador de versió:

// La teva memoïtzació de 09-02, amb la sintaxi de React
const visibles = useMemo(
  () => ordenarPerPrioritat(tasques.filter((t) => responsable === null || t.responsable === responsable)),
  [tasques, responsable]
);

useCallback(fn, deps) és literalment useMemo(() => fn, deps). Existeix perquè el cas és tan freqüent que mereixia drecera:

const marcarFeta = useCallback((id) => {
  setTasques((actuals) => actuals.map((t) => (t.id === id ? { ...t, estat: 'feta' } : t)));
}, []);   // sense dependències: fa servir la forma de funció de setTasques, no llegeix res de fora

Fixa't en l'array buit: és possible precisament perquè setTasques rep una funció en lloc de llegir tasques del closure. Aquest patró és el que fa que les funcions siguin estables de debò.

useRef és diferent dels altres dos, i té dos usos que no s'assemblen:

// Ús 1: accedir a un node real del DOM (per mesurar, enfocar, reproduir)
function CampCerca() {
  const camp = useRef(null);

  useEffect(() => { camp.current.focus(); }, []);

  return <input ref={camp} type="search" />;
}

// Ús 2: guardar un valor mutable que NO ha de provocar renderitzat
function Cronometre() {
  const idInterval = useRef(null);
  // canviar idInterval.current no torna a pintar res
}

L'ús 1 és la teva escotilla d'emergència cap al DOM real: quan necessites mesurar amb getBoundingClientRect (09-04), enfocar un camp o integrar una llibreria que espera un node. És legítim i de vegades imprescindible, però cada ref que modifica el DOM directament és un tros de pantalla que surt del model UI = f(estat).

  1. No optimitzis sense mesurar

Aquí convé ser taxatiu, perquè és on més temps es perd a React i on 09-01 té més coses a dir.

useMemo i useCallback no són gratis. Cadascun guarda un valor i compara un array de dependències a cada renderitzat. Envoltar un càlcul trivial costa més que el càlcul:

// ❌ Absurd: comparar l'array de dependències costa més que la suma
const total = useMemo(() => a + b, [a, b]);

// ❌ Gairebé sempre inútil: el fill no està memoïtzat, així que es repinta igualment
const enPremer = useCallback(() => setObert(true), []);

Aquest segon cas és el més comú i el més inútil: useCallback només aporta alguna cosa si el destinatari de la funció compara les seves props, és a dir, si està embolcallat en React.memo o si la funció és dependència d'un efecte. Si no, estàs pagant per una estabilitat que ningú no fa servir.

El procediment correcte és el de 09-01, sense dreceres:

  1. Mesura. El React DevTools Profiler grava una interacció i mostra quins components s'han renderitzat, quantes vegades i quant han trigat. És l'equivalent del panell Performance per a React.
  2. Troba el component car. Gairebé sempre n'hi ha un: una llista llarga, un gràfic, un càlcul pesat.
  3. Comprova per què es renderitza. El Profiler diu quina prop ha canviat. Moltes vegades la resposta és «una funció nova a cada render» i allà sí que useCallback serveix.
  4. Aplica l'optimització mínima i torna a mesurar.

I el mateix avís de 09-04: l'optimització que més rendeix en llistes llargues no és memoïtzar, és no pintar 600 files. La virtualització que vas construir continua sent la resposta correcta, amb llibreries que la implementen per tu.

Una nota de futur que estalvia ansietat: el compilador de React (apartat 23) està dissenyat precisament per inserir aquestes memoïtzacions automàticament i fer innecessària la major part d'aquesta feina manual.

  1. Hooks personalitzats: useTauler i useFiltreResponsable

Un hook personalitzat és una funció el nom de la qual comença per use i que crida altres hooks. No hi ha més definició. El seu valor és enorme: permet extreure lògica amb estat —no només càlculs— i reutilitzar-la entre components.

És l'equivalent del que els teus mòduls util/ feien amb funcions pures, però per a lògica que té estat i cicle de vida.

// src/hooks/useTauler.js
import { useState, useCallback, useMemo } from 'react';
import { SEGUENT, ETIQUETA } from '../domini/regles.js';

/**
 * Estat del tauler de Nómada Tasques, amb les seves operacions.
 * Les regles de negoci (R5, R6) viuen a domini/regles.js, fora de React.
 */
export function useTauler(tasquesInicials) {
  const [tasques, setTasques] = useState(tasquesInicials);

  const canviarEstat = useCallback((id, desti) => {
    setTasques((actuals) =>
      actuals.map((t) => {
        if (t.id !== id) return t;                    // mateixa referència: no canvia
        if (SEGUENT[t.estat] !== desti) return t;      // R6: transició no permesa
        return { ...t, estat: desti };                 // objecte nou (04-07)
      })
    );
  }, []);

  const marcarFeta = useCallback((id) => canviarEstat(id, 'feta'), [canviarEstat]);

  const resum = useMemo(() => {
    const obertes = tasques.filter((t) => t.estat !== 'feta');
    return {
      total: tasques.length,
      horesTotals: tasques.reduce((s, t) => s + t.horesEstimades, 0),
      horesObertes: obertes.reduce((s, t) => s + t.horesEstimades, 0),
      pendents: tasques.filter((t) => t.estat === 'pendent').length
    };
  }, [tasques]);

  return { tasques, canviarEstat, marcarFeta, resum };
}
// src/hooks/useFiltreResponsable.js
import { useState, useMemo } from 'react';

/** Filtre per responsable: el seu estat, la llista de responsables i el resultat. */
export function useFiltreResponsable(tasques) {
  const [responsable, setResponsable] = useState(null);

  const responsables = useMemo(
    () => [...new Set(tasques.map((t) => t.responsable).filter(Boolean))].sort(),
    [tasques]
  );

  const visibles = useMemo(
    () => (responsable === null ? tasques : tasques.filter((t) => t.responsable === responsable)),
    [tasques, responsable]
  );

  return { responsable, setResponsable, responsables, visibles };
}

Cinc coses per aprendre d'aquests dos hooks:

  1. Retornen un objecte, no un array. Els arrays són còmodes quan hi ha dos valors (com useState); amb cinc, un objecte és autodocumentat.
  2. Les regles de negoci són fora. SEGUENT i les R1–R10 viuen a domini/regles.js, en JavaScript pur. És la disciplina de 10-01: la vista és del framework, el model és teu. Si demà canvies a Vue, aquest fitxer no es toca.
  3. Cada crida al hook crea el seu propi estat. Dos components que facin servir useFiltreResponsable tenen dos filtres independents. Un hook no comparteix estat: comparteix lògica. Confondre això és el malentès número u sobre els hooks.
  4. useMemo aquí sí que té un motiu, i no és el rendiment del càlcul: és mantenir estable la referència de visibles i responsables perquè els components fills memoïtzats no es repintin sense motiu.
  5. new Set(...) de 04-05 elimina duplicats i filter(Boolean) descarta els null de la R8: dues utilitats de JavaScript pur que es fan servir igual dins de React.

  1. Les regles dels hooks i per què existeixen

Dues regles, i una explicació que gairebé mai no es dona:

  1. Només es criden al nivell superior d'un component o d'un altre hook. Mai dins d'un if, un bucle, un try o una funció imbricada.
  2. Només es criden des de components de React o des de hooks personalitzats.

El motiu de la primera és purament mecànic. React no sap com es diuen les teves variables d'estat: guarda els valors en una llista i els identifica per l'ordre de crida. La primera crida a useState d'aquest component és la posició 0, la segona la posició 1, i així.

// ❌ Això trenca la correspondència
function Component({ mostrar }) {
  if (mostrar) {
    const [a, setA] = useState(1);   // ← de vegades és la posició 0, de vegades no existeix
  }
  const [b, setB] = useState(2);      // ← de vegades posició 1, de vegades posició 0
}

Quan mostrar canvia de true a false, b rep el valor que era d'a. No hi ha error, hi ha dades creuades. Per això la regla és absoluta i per això existeix la regla d'ESLint react-hooks/rules-of-hooks, que ha d'estar activada.

La manera correcta de tenir un hook condicional és extreure el tros condicional al seu propi component, que de vegades es pinta i de vegades no.

  1. Formularis controlats i no controlats

Dos enfocaments, i l'elecció es fa per un criteri clar.

Controlat: el valor del camp viu a l'estat de React. El camp només mostra el que l'estat diu.

function CercadorTasques({ enCercar }) {
  const [text, setText] = useState('');

  return (
    <input
      type="search"
      value={text}                                 // ← la font de la veritat és l'estat
      onChange={(e) => setText(e.target.value)}     // ← cada tecla actualitza l'estat
      placeholder="Cercar tasques"
    />
  );
}

No controlat: el valor viu al DOM, com de tota la vida, i es llegeix quan cal.

function FormulariTasca({ enCrear }) {
  const formulari = useRef(null);

  function enviar(e) {
    e.preventDefault();
    const dades = Object.fromEntries(new FormData(e.currentTarget));   // 06-07
    enCrear(dades);
    e.currentTarget.reset();
  }

  return (
    <form ref={formulari} onSubmit={enviar}>
      <input name="titol" required minLength={3} />
      <input name="horesEstimades" type="number" min={1} max={40} required />
      <button type="submit">Crear tasca</button>
    </form>
  );
}

Fixa't en el segon: FormData i Object.fromEntries són exactament els de 06-07, i els atributs required, min i max són la validació nativa del navegador implementant les regles R2 i R3. No cal React per a això, i fer-lo servir és més simple, més accessible i més ràpid.

Criteri Controlat No controlat
Font de la veritat L'estat de React El DOM
Renderitzats en escriure Un per tecla Cap
Validació en viu mentre s'escriu Fàcil Difícil
Inhabilitar el botó segons el contingut Fàcil Difícil
Formatar mentre s'escriu (telèfon, quantitat) Fàcil Molt difícil
Formulari gran i senzill Costós Ideal
Integració amb la validació nativa d'HTML Se'n perd part Completa

La regla pràctica: controlat quan la interfície ha de reaccionar al que s'escriu; no controlat quan només importa el valor final. Un cercador amb filtre en viu és controlat (i amb el debounce de 09-02, que continua sent necessari). Un formulari d'alta de tasca és perfectament no controlat.

  1. El DOM virtual i la reconciliació, de debò

Ja saps què és un element de React (apartat 3) i per què necessita key (apartat 7). Falta ajuntar les peces del mecanisme complet, perquè explica tots els comportaments estranys.

Quan canvia l'estat d'un component, React fa això:

graph TD
  S[Canvia l'estat] --> R["Renderitzat: s'executa la funció<br/>del component i les dels seus fills"]
  R --> A["Arbre nou d'elements"]
  A --> D["Reconciliació: comparar<br/>arbre nou amb arbre anterior"]
  D --> L["Llista d'operacions mínimes"]
  L --> C["Confirmació: aplicar-ho al DOM real,<br/>executar neteges i efectes"]

Les tres regles de l'algorisme de comparació, que són heurístiques i no un algorisme òptim:

Regla 1 · Tipus diferents, subarbre nou. Si a la mateixa posició hi havia un <div> i ara hi ha una <section>, React no compara res a dins: destrueix tot el subarbre i el crea de nou. Es perd l'estat de tots els components de dins. Això explica una fallada desconcertant:

// ❌ El component canvia de tipus segons la condició: es perd l'estat intern
{compacte ? <div><Llista tasques={t}/></div> : <section><Llista tasques={t}/></section>}

Regla 2 · Mateix tipus, s'actualitzen els atributs i es compara a dins. El node del DOM es conserva; només canvien les propietats que difereixen. És el que fa el teu pintarTargeta(tasca, existent) quan rep un node.

Regla 3 · A les llistes, s'aparella per key; sense key, per posició. L'apartat 7 sencer.

El que costa, amb honestedat:

  • S'executen les funcions de tots els components afectats, encara que el resultat sigui idèntic. Amb 600 targetes, canviar el filtre executa 600 funcions per descobrir que 594 no han canviat. És el cost inherent del DOM virtual que anunciava 10-01: proporcional a quant n'hi ha, no a quant ha canviat.
  • Es creen 600 objectes descriptors que seran escombraries en el següent cicle. El recol·lector de 09-03 té feina.
  • A canvi, les operacions sobre el DOM real sí que són mínimes, que és on hi ha el cost car (recàlcul d'estil i disposició, 09-04).

Aquest és l'intercanvi exacte: més feina en JavaScript, menys al DOM. Com que a la teva aplicació vas mesurar que el DOM era el coll d'ampolla i no el JavaScript, l'intercanvi acostuma a sortir a favor. Però no sempre: amb llistes molt grans o actualitzacions molt freqüents, es nota, i per això existeixen React.memo, useMemo i la virtualització.

Una nota sobre el renderitzat concurrent: React modern pot interrompre un renderitzat en curs si arriba alguna cosa més urgent (una pulsació de tecla), i reprendre'l després. Això és el que fan useTransition i useDeferredValue, que són l'equivalent conceptual del teu trossejat amb perLots de 09-02: evitar que una actualització gran bloquegi el fil principal. No ho desenvolupem aquí, però en reconeixeràs el problema.

  1. El compilador de React i els components de servidor

Dues evolucions importants que convé conèixer per no confondre's en llegir codi actual, sense desenvolupar-les.

El compilador de React. Analitza els teus components durant la construcció i insereix automàticament les memoïtzacions que avui s'escriuen a mà amb useMemo, useCallback i React.memo. Si funciona bé, l'apartat 18 es converteix en una anècdota històrica: escrius codi simple i el compilador l'optimitza, que és exactament l'enfocament de la família 3 de 10-01 aplicat a un framework de DOM virtual. La condició perquè funcioni és que els teus components siguin purs —el que es demanava a l'apartat 4—, amb la qual cosa la disciplina que estàs aprenent ara és el que permet l'optimització de demà.

Els components de servidor. Components que s'executen només al servidor i envien al navegador el resultat, no el seu codi. Avantatges: zero JavaScript descarregat per a les parts que no són interactives, i accés directe a la base de dades sense passar per una API. Impliquen una distinció nova entre components de servidor i de client, amb regles pròpies sobre què pot fer cadascun, i a la pràctica requereixen un meta-framework com Next.js. És un tema gran i en moviment; el just aquí és saber que existeix i que resol el problema del pes descarregat, que 10-06 reprendrà en parlar de renderitzat al servidor.

  1. Carregar dades amb useEffect i els seus tres problemes

Aquest és l'apartat més útil de la lliçó per al món real. La manera «òbvia» de carregar dades és aquesta, i té tres fallades serioses:

// ❌ La versió ingènua, amb tres problemes
function LlistaRemota({ responsable }) {
  const [tasques, setTasques] = useState([]);
  const [carregant, setCarregant] = useState(true);

  useEffect(() => {
    setCarregant(true);
    llistarTasques({ responsable })
      .then((dades) => { setTasques(dades); setCarregant(false); });
  }, [responsable]);

  if (carregant) return <p>Carregant…</p>;
  return <LlistaTasques tasques={tasques} />;
}

Problema 1 · Condició de cursa. La Marta canvia el filtre d'«Iván» a «Lucía» ràpidament. Es llancen dues peticions. La d'Iván triga 900 ms; la de Lucía, 200 ms. La de Lucía torna primer i pinta les seves tasques; després torna la d'Iván i sobreescriu. La pantalla mostra les tasques d'Iván amb el filtre a «Lucía». És una fallada real, intermitent i molt difícil de reproduir.

Problema 2 · Sense cancel·lació. Si el component es desmunta mentre la petició vola, es crida setTasques sobre un component que ja no existeix: feina malgastada i, amb recursos pesats, una fuita de les de 09-03.

Problema 3 · Doble execució en mode estricte. En desenvolupament, <StrictMode> munta, desmunta i torna a muntar cada component a propòsit. Veuràs dues peticions a la pestanya Network. No és una fallada de React: és un detector d'efectes que no netegen bé. Si el teu efecte és correcte, la doble execució és innòcua.

La versió correcta fa servir el que ja saps de 07-03:

// ✅ Amb cancel·lació i protecció contra curses
function LlistaRemota({ responsable }) {
  const [estat, setEstat] = useState({ fase: 'carregant', tasques: [], error: null });

  useEffect(() => {
    const controlador = new AbortController();
    setEstat((e) => ({ ...e, fase: 'carregant' }));

    llistarTasques({ responsable, signal: controlador.signal })
      .then((tasques) => setEstat({ fase: 'llest', tasques, error: null }))
      .catch((error) => {
        if (error.name === 'AbortError') return;      // cancel·lació esperada: no és una fallada
        setEstat({ fase: 'error', tasques: [], error });
      });

    return () => controlador.abort();                  // ← neteja: mata la petició anterior
  }, [responsable]);

  if (estat.fase === 'carregant') return <p role="status">Carregant tasques…</p>;
  if (estat.fase === 'error') return <p role="alert">No s&apos;han pogut carregar: {estat.error.message}</p>;
  return <LlistaTasques tasques={estat.tasques} />;
}

Les tres correccions, una per problema:

  1. La cursa desapareix perquè la neteja avorta la petició anterior abans de llançar la nova. La resposta d'Iván no arriba mai a then perquè la seva promesa es rebutja amb AbortError.
  2. La cancel·lació la dona el mateix AbortController de 07-03, passat a demanarJson com a signal.
  3. La doble execució ja no molesta: la primera petició s'avorta en desmuntar i només compta la segona.

I fixa't en l'estat: una sola variable amb fase, en lloc de tres booleans independents. És la regla de l'estat impossible de l'apartat 11.

  1. Per què en producció es fa servir una llibreria de dades

El codi anterior és correcte i són vint línies. Ara multiplica'l per les quinze pantalles d'una aplicació real i apareixen preguntes que aquell codi no respon:

  • Si dos components demanen les mateixes tasques alhora, es fan dues peticions?
  • En tornar a una pantalla visitada fa deu segons, es mostra el que hi havia mentre es refresca, o una pantalla de càrrega?
  • Quan la Marta torna a la pestanya al cap d'una hora, es refresquen les dades?
  • En crear una tasca, qui invalida la llista perquè hi aparegui?
  • Si la xarxa falla, es reintenta? Quantes vegades?
  • Amb optimistic UI (07-03), qui reverteix si el servidor ho rebutja?

Respondre a tot això a mà, a cada pantalla, és escriure una memòria cau d'estat de servidor. I això ja existeix: TanStack Query és la més usada a React (amb equivalents a Vue, Angular i Svelte). El concepte, que és el que importa aquí:

// Presentat com a concepte, no com a tutorial
const { data: tasques, isPending, error } = useQuery({
  queryKey: ['tasques', responsable],           // identitat d'aquesta consulta a la memòria cau
  queryFn: ({ signal }) => llistarTasques({ responsable, signal })
});

El que aporta, i que connecta directament amb l'apartat 15 de 10-01:

Aporta Què resol
Memòria cau per clau Dos components amb la mateixa queryKey comparteixen una petició
Dades obsoletes mentre revalida Es mostra el que hi havia i es refresca per darrere: res de pantalles de càrrega en tornar enrere
Refresc automàtic En enfocar la finestra, en reconnectar, per interval
Reintents Amb retrocés exponencial, com el teu ambReintents de 07-03
Cancel·lació Passa el signal automàticament
Invalidació Després de crear una tasca, es marca ['tasques'] com a obsoleta i es recarrega sola
Mutacions optimistes Amb reversió automàtica si falla

L'important no és la llibreria: és la idea de 10-01 que l'estat del servidor és un problema diferent i mereix una eina pròpia. Tan bon punt ho assumeixes, l'estat que queda per a React —o per a Redux a 10-03— és molt menor del que semblava.

  1. L'ecosistema mínim

El que necessita un projecte React de debò, més enllà de React:

Peça Opció habitual Què ja saps
Empaquetament Vite 09-05, tal qual
Encaminament React Router / TanStack Router / meta-framework La History API de 07-06 i el teu encaminador.js
Dades remotes TanStack Query sobre el teu demanarJson 07-02, 07-03
Estat global Redux Toolkit, Zustand, Jotai 10-03
Formularis React Hook Form, o FormData a mà 06-07
Qualitat ESLint amb react-hooks, Prettier 08-02
Proves Vitest o Jest + React Testing Library 08-03, 08-05
Extrem a extrem Cypress o Playwright 08-06

Val la pena aturar-se en les proves, perquè aquí no aprens res de nou: Testing Library funciona igual. A 08-05 vas escriure proves que renderitzaven la vista, consultaven per rol i simulaven clics. A React és idèntic:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('en filtrar per Lucía només queda la seva tasca', async () => {
  render(<App tasquesInicials={BACKLOG} />);

  expect(screen.getAllByRole('listitem')).toHaveLength(6);

  await userEvent.selectOptions(screen.getByLabelText(/responsable/i), 'Lucía');

  expect(screen.getAllByRole('listitem')).toHaveLength(1);
  expect(screen.getByText('Actualitzar el web de reserves')).toBeInTheDocument();
});

Les consultes per rol, userEvent, la filosofia de provar el que veu la usuària i no els detalls interns: tot igual que a 08-05. És una de les coses que millor es transfereixen entre JavaScript pur i qualsevol framework, i una raó més per haver après primer el que hi ha a sota.

  1. Nómada Tasques en React: la llista completa

Aquí tens la reimplementació completa de la pantalla objectiu: la llista de tasques, amb filtre per responsable i botó de marcar com a feta. Quatre fitxers de components, dos hooks i un fitxer de domini en JavaScript pur.

Primer el domini, que no és React i que no canviaria en canviar de framework:

// src/domini/regles.js — JavaScript pur, sense dependències
export const SEGUENT = Object.freeze({
  pendent: 'en-curs',
  'en-curs': 'feta',
  feta: null
});

export const ETIQUETA = Object.freeze({
  pendent: 'Començar',
  'en-curs': 'Marcar feta',
  feta: 'Feta'
});

export const PESOS = Object.freeze({ alta: 3, mitjana: 2, baixa: 1 });

export const AVUI = '2026-09-20';

/** R10: vençuda = data límit passada i estat diferent de 'feta'. */
export function estaVencuda(tasca, avui = AVUI) {
  return tasca.estat !== 'feta' && tasca.dataLimit < avui;
}

export function horesObertes(tasques) {
  return tasques.filter((t) => t.estat !== 'feta')
                .reduce((suma, t) => suma + t.horesEstimades, 0);
}

Ara la targeta:

// src/components/TargetaTasca.jsx
import { memo } from 'react';
import { SEGUENT, ETIQUETA, estaVencuda } from '../domini/regles.js';

export const TargetaTasca = memo(function TargetaTasca({ tasca, enAvancar }) {
  const vencuda = estaVencuda(tasca);
  const seguent = SEGUENT[tasca.estat];

  return (
    <li
      className={`tasca tasca--${tasca.prioritat}
                  ${tasca.estat === 'feta' ? 'tasca--feta' : ''}
                  ${vencuda ? 'tasca--vencuda' : ''}`}
      data-id={tasca.id}
    >
      <h3 className="tasca__titol">{tasca.titol}</h3>

      <p className="tasca__meta">
        {tasca.responsable ?? 'sense assignar'} · {tasca.horesEstimades} h
        {vencuda && <span className="tasca__avis"> · ⚠ vençuda</span>}
      </p>

      <ul className="tasca__etiquetes">
        {tasca.etiquetes.map((e) => (
          <li key={e} className="etiqueta">{e}</li>
        ))}
      </ul>

      <button
        type="button"
        disabled={seguent === null}
        onClick={() => enAvancar(tasca.id, seguent)}
        aria-label={`${ETIQUETA[tasca.estat]}: ${tasca.titol}`}
      >
        {ETIQUETA[tasca.estat]}
      </button>
    </li>
  );
});

Punts a subratllar:

  • memo fa que la targeta només es torni a renderitzar si les seves props canvien per referència. Aquí sí que té sentit, perquè useTauler retorna el mateix objecte tasca per a les que no canvien (gràcies al map immutable de l'apartat 19) i enAvancar és estable gràcies a useCallback. Sense aquestes dues condicions, memo no serviria de res.
  • data-id es conserva. React no el necessita —per a això hi ha la key—, però el necessiten les teves proves de Cypress de 08-06 i fa que el DOM continuï sent llegible.
  • La llista d'etiquetes té la seva pròpia key: el text de l'etiqueta, que és únic dins de la tasca per la R9 (minúscules i sense duplicats). Aquesta regla, que vas escriure per netedat de dades, resulta ser justament el que fa vàlida la clau.
  • El botó s'inhabilita quan SEGUENT[estat] és null, que és la R6 aplicada a la vista.

El filtre:

// src/components/FiltreResponsable.jsx
export function FiltreResponsable({ responsables, valor, enCanviar }) {
  return (
    <label className="filtre" htmlFor="filtre-responsable">
      Responsable:
      <select
        id="filtre-responsable"
        value={valor ?? ''}
        onChange={(e) => enCanviar(e.target.value || null)}
      >
        <option value="">Tots</option>
        {responsables.map((nom) => (
          <option key={nom} value={nom}>{nom}</option>
        ))}
      </select>
    </label>
  );
}

Detall important: value={valor ?? ''} i e.target.value || null. L'estat fa servir null per a «sense filtre» —coherent amb la R8, que prohibeix la cadena buida— però el DOM només entén cadenes. La conversió es fa a la frontera, en tots dos sentits.

La llista:

// src/components/LlistaTasques.jsx
import { TargetaTasca } from './TargetaTasca.jsx';

export function LlistaTasques({ tasques, enAvancar }) {
  if (tasques.length === 0) {
    return <p className="llista-buida">Cap tasca no coincideix amb el filtre.</p>;
  }

  return (
    <ul className="llista-tasques">
      {tasques.map((tasca) => (
        <TargetaTasca key={tasca.id} tasca={tasca} enAvancar={enAvancar} />
      ))}
    </ul>
  );
}

I l'aplicació que ho ajunta tot:

// src/App.jsx
import { useTauler } from './hooks/useTauler.js';
import { useFiltreResponsable } from './hooks/useFiltreResponsable.js';
import { FiltreResponsable } from './components/FiltreResponsable.jsx';
import { LlistaTasques } from './components/LlistaTasques.jsx';
import { horesObertes } from './domini/regles.js';

export default function App({ tasquesInicials }) {
  const { tasques, canviarEstat, resum } = useTauler(tasquesInicials);
  const { responsable, setResponsable, responsables, visibles } = useFiltreResponsable(tasques);

  return (
    <main className="tauler">
      <header className="tauler__capcalera">
        <h1>Nómada Tasques</h1>
        <FiltreResponsable
          responsables={responsables}
          valor={responsable}
          enCanviar={setResponsable}
        />
        <p className="tauler__resum">
          {visibles.length} de {resum.total} tasques · {horesObertes(visibles)} h obertes
        </p>
      </header>

      <LlistaTasques tasques={visibles} enAvancar={canviarEstat} />
    </main>
  );
}

Comprova els números canònics: sense filtre, 6 de 6 tasques i 45 h obertes. Amb «Iván», 3 de 6 i 25 h. Amb «Lucía», 1 de 6 i 14 h. Amb «Marta», 2 de 6 i 6 h obertes (la d'inventari està feta i no suma). I en prémer «Començar» a Pressupost de la fusteria, la targeta canvia d'estat, deixa d'estar vençuda tan bon punt arribi a feta, i les hores obertes baixen a 40 — sense que ningú hagi escrit ni una sola línia que actualitzi el resum. Aquest és el model declaratiu funcionant.

  1. Comparació costat a costat amb tauler-vista.js

Ara la comparació honesta amb el que ja tenies.

Concepte JavaScript pur (Nómada Tasques) React
Descriure la pantalla pintarTargeta(tasca, existent, avui) + <template> <TargetaTasca tasca={t}/>
Identitat a les llistes data-id + reconciliar() (20 línies escrites per tu) key={t.id} (una propietat)
Tornar a pintar Cridar render() a mà Cridar setEstat; el renderitzat és automàtic
Estat local d'un tros No hi ha lloc natural: dataset o un Map extern useState dins del component
Escoltar esdeveniments Delegació amb closest('[data-accio]') onClick a cada element (React delega per dins)
Comunicar cap amunt CustomEvent + ESDEVENIMENTS Prop de funció
Neteja destruir() + AbortController, invocat a mà return de l'efecte, invocat per React
Memòria cau de derivats Memòria cau per versió amb #versio useMemo amb dependències
Estils Classes globals a estils.css Classes globals, mòduls CSS o similars
Reutilitzar lògica amb estat Classes o closures Hooks personalitzats

I la comparació quantitativa de la mateixa pantalla —llista amb filtre i botó d'avançar—, amb els fitxers d'aquest apartat davant dels equivalents del teu projecte:

Mètrica JavaScript pur React
Línies de codi de la vista ~210 (dom.js + targeta.js + tauler-vista.js + controlador.js + <template>) ~130 (4 components + 2 hooks)
Línies d'infraestructura escrites per tu ~60 (reconciliar, crearElement, $, $$, delegació) 0
Línies de domini (reutilitzables en qualsevol cas) ~40 ~40 (les mateixes)
Dependències de producció 0 2 (react, react-dom)
JavaScript descarregat (comprimit, aprox.) 58,3 kB tota l'aplicació +45 kB només el motor
Fitxers a tocar per afegir una dada a la targeta 2 (<template> i targeta.js) 1 (TargetaTasca.jsx)
Conceptes que cal conèixer per llegir-ho DOM, esdeveniments, ES modules Tot l'anterior més JSX, hooks, regles dels hooks, reconciliació

Què s'ha guanyat, sense adorns:

  • Desapareixen 60 línies d'infraestructura que a més s'han de mantenir i provar.
  • L'estat local per component té per fi on viure.
  • La neteja està garantida, no confiada a la memòria.
  • Cada component és una unitat llegible, amb el seu contracte a la primera línia.
  • La reconciliació funciona per a arbres imbricats, no només per a llistes planes de fills directes — que és on el teu reconciliar es quedava curt.

Què s'ha perdut, amb la mateixa franquesa:

  • 45 kB de motor descarregat abans de veure res. En una aplicació d'aquesta escala, és més que tota la lògica.
  • Un pas de compilació obligatori: sense compilador no hi ha JSX.
  • Conceptes nous que cal aprendre i depurar: per què s'executa dues vegades, per què aquesta dependència canvia sempre, per què això no s'actualitza.
  • Control fi sobre el DOM: la teva virtualització de 09-04, els teus lots d'escriptura i les teves mesures amb requestAnimationFrame ara s'han de fer a través de React, amb ref i llibreries, en lloc de directament.
  • Independència: el 60 % d'aquest codi només val dins de React.

La conclusió honesta per a Nómada Tasques: amb sis tasques, una pantalla i un desenvolupador, React no compensa. Amb cinc pantalles, quatre persones i trenta components reutilitzables, compensa clarament. I aquest canvi de resposta segons el context —no un guanyador absolut— és exactament el que 10-06 formalitzarà.

Errors Habituals i Consells

Fer servir l'índex de l'array com a key. Funciona fins al dia que es filtra, es reordena o s'elimina alguna cosa pel mig; llavors produeix fallades subtils de focus i d'estat. Si tens id, fes-lo servir. És el mateix error que hauries comès fent servir la posició al teu reconciliar.

Mutar l'estat. tasques.push(nova) o tasca.estat = 'feta' no provoquen renderitzat, perquè la referència no canvia. Fes servir spread i map com a 04-07. Si l'estat és un objecte imbricat profund, és senyal que està mal modelat.

Esperar que l'estat canviï immediatament després de setEstat. No canvia: la variable actual és una constant capturada en aquesta execució. El valor nou arriba en el renderitzat següent. Si necessites el valor anterior per calcular el nou, fes servir la forma de funció: setComptador((n) => n + 1).

Posar objectes literals a l'array de dependències. [{ responsable }] és un objecte nou a cada render i l'efecte es dispara sempre. Depèn de primitius: [responsable].

Silenciar exhaustive-deps. És la regla que evita que un efecte llegeixi valors obsolets. Quan molesta, gairebé sempre el problema és el disseny de l'efecte. Deshabilitar-la converteix un avís en una fallada intermitent.

Fer servir useEffect per derivar estat. Dos renderitzats, dues fonts de veritat i un fotograma amb el valor vell. Calcula durant el renderitzat; memoïtza només si mesures que fa mal.

Memoïtzar-ho tot «per si de cas». useMemo i useCallback tenen cost i embruten el codi. Sense React.memo al destinatari, useCallback no serveix de res. Mesura amb el Profiler abans, com a 09-01.

Espantar-se per la doble execució en desenvolupament. <StrictMode> munta i desmunta a propòsit per destapar efectes que no netegen. Si el teu efecte està ben escrit, és innocu. Si de debò et molesta, és que hi ha alguna cosa per arreglar.

Carregar dades sense AbortController. És la condició de cursa de l'apartat 24, i produeix fallades que només apareixen amb la xarxa lenta. Tot el de 07-03 continua sent tan necessari com abans.

Consell: mantén la lògica de negoci fora dels components. domini/regles.js és JavaScript pur i es prova amb Jest sense renderitzar res. És la disciplina de 10-01 i és el que fa reversible l'elecció de framework.

Consell: comença sense memoïtzació i sense llibreries. useState, props i càlcul directe arriben molt més lluny del que sembla. Afegeix complexitat quan el Profiler o el dolor real ho justifiquin.

Exercicis

Exercici 1 · Afegir el resum per responsable

Partint del codi de l'apartat 27, afegeix un component <ResumPerResponsable> que mostri, per a cada persona de l'equip, quantes tasques obertes té i quantes hores sumen. Ha de respectar els números canònics: Iván 3 tasques / 25 h, Lucía 1 / 14 h, Marta 1 / 6 h.

Requisits:

  • El càlcul es fa durant el renderitzat, no en un efecte.
  • Fes servir Object.groupBy o reduce, com a 04-05.
  • El component rep les tasques per props i no té estat propi.
  • Afegeix una key correcta a la llista i justifica la teva elecció.

Pregunta addicional: hauria de mostrar aquest component el total de totes les tasques o només el de les visibles després del filtre? Justifica-ho.

Exercici 2 · Caçar l'error de la condició de cursa

Aquest component té tres fallades diferents. Troba-les, explica quan es manifesta cadascuna i escriu la versió corregida.

function DetallTasca({ id }) {
  const [tasca, setTasca] = useState(null);
  const [comentaris, setComentaris] = useState([]);

  useEffect(() => {
    fetch(`/api/tasques/${id}`)
      .then((r) => r.json())
      .then(setTasca);
  }, []);

  useEffect(() => {
    if (tasca) {
      setComentaris(tasca.comentaris.filter((c) => !c.esborrat));
    }
  }, [tasca]);

  return (
    <article>
      <h2>{tasca?.titol}</h2>
      {comentaris.map((c, i) => <p key={i}>{c.text}</p>)}
    </article>
  );
}

Exercici 3 · El hook useDebounce i el cercador

A 09-02 vas escriure una funció debounce a js/util/temps.js perquè el cercador no filtrés a cada tecla. Ara fes-ho a la manera de React:

  1. Escriu un hook useDebounce(valor, retard) que retorni el valor amb retard, fent servir useState i useEffect amb la seva neteja.
  2. Fes-lo servir en un component <CercadorTasques> amb un <input> controlat que filtri la llista per títol.
  3. Explica per què l'<input> ha de fer servir el valor immediat i el filtre el valor retardat, i què passaria si es fes servir el retardat als dos llocs.
  4. Per què aquest hook necessita obligatòriament la funció de neteja? Què passaria sense ella?

Solucions

Solució 1

// src/components/ResumPerResponsable.jsx
export function ResumPerResponsable({ tasques }) {
  // Càlcul durant el renderitzat: sense efectes, sense estat.
  const perPersona = tasques
    .filter((t) => t.estat !== 'feta')             // només obertes
    .reduce((acc, t) => {
      const nom = t.responsable ?? 'sense assignar';   // R8: null, mai ''
      const previ = acc[nom] ?? { tasques: 0, hores: 0 };
      acc[nom] = {
        tasques: previ.tasques + 1,
        hores: previ.hores + t.horesEstimades
      };
      return acc;
    }, {});

  const files = Object.entries(perPersona).sort(([a], [b]) => a.localeCompare(b, 'ca'));

  return (
    <ul className="resum-responsables">
      {files.map(([nom, { tasques: n, hores }]) => (
        <li key={nom}>
          <strong>{nom}</strong>: {n} {n === 1 ? 'tasca' : 'tasques'} · {hores} h
        </li>
      ))}
    </ul>
  );
}

La key: el nom del responsable. És única dins d'aquesta llista (és la clau de l'objecte agrupat) i és estable: si l'Iván passa de tres a dues tasques, la seva fila conserva el mateix node i no parpelleja. L'índex seria mala idea perquè la llista es reordena quan apareixen o desapareixen persones en filtrar.

Amb el backlog canònic, sense filtre: Iván 3 tasques / 25 h, Lucía 1 / 14 h, Marta 1 / 6 h. Total 5 tasques obertes i 45 h, que quadra (la sisena, Inventari de tintes de la Marta, està feta i no compta).

Totals o visibles? Ha de rebre totes les tasques, no les visibles. El motiu és de significat: el resum per responsable existeix per respondre «com està repartida la càrrega del taller?», i aquesta pregunta no depèn de què estigui mirant la Marta ara mateix. Si rebés les visibles, en filtrar per Iván el resum mostraria només l'Iván, cosa que és tautològica i inútil. És un bon exemple que quines props passes és una decisió de producte, no tècnica:

<ResumPerResponsable tasques={tasques} />       {/* totes */}
<LlistaTasques tasques={visibles} … />          {/* filtrades */}

Solució 2

Fallada 1 · Dependència que falta ([] en lloc de [id]). Es manifesta quan l'usuari navega d'una tasca a una altra sense desmuntar el component: es continua mostrant la primera tasca per sempre, perquè l'efecte no es torna a executar. És exactament el que detecta exhaustive-deps.

Fallada 2 · Sense cancel·lació ni control d'errors. Dues manifestacions: la condició de cursa de l'apartat 24 si es canvia de tasca ràpidament amb la xarxa lenta, i una fallada silenciosa si fetch rebutja o si el servidor respon 404 (recorda de 07-02 que fetch no rebutja davant d'un 404: r.json() fallarà en intentar analitzar la resposta).

Fallada 3 · Estat derivat amb un efecte (el segon useEffect) i key per índex als comentaris. comentaris es dedueix de tasca, així que es calcula durant el renderitzat. I fer servir l'índex com a clau fa que, en esborrar-se un comentari del mig, tots els següents heretin nodes aliens.

Versió corregida:

function DetallTasca({ id }) {
  const [estat, setEstat] = useState({ fase: 'carregant', tasca: null, error: null });

  useEffect(() => {
    const controlador = new AbortController();
    setEstat({ fase: 'carregant', tasca: null, error: null });

    demanarJson(`/api/tasques/${id}`, { signal: controlador.signal })   // 07-03: llança ErrorDeApi
      .then((tasca) => setEstat({ fase: 'llest', tasca, error: null }))
      .catch((error) => {
        if (error.name === 'AbortError') return;
        setEstat({ fase: 'error', tasca: null, error });
      });

    return () => controlador.abort();
  }, [id]);                                    // ← fallada 1 corregida

  if (estat.fase === 'carregant') return <p role="status">Carregant…</p>;
  if (estat.fase === 'error') return <p role="alert">{estat.error.message}</p>;

  // ← fallada 3 corregida: derivat durant el renderitzat
  const comentaris = estat.tasca.comentaris.filter((c) => !c.esborrat);

  return (
    <article>
      <h2>{estat.tasca.titol}</h2>
      {comentaris.map((c) => <p key={c.id}>{c.text}</p>)}   {/* clau estable */}
    </article>
  );
}

Nota addicional: demanarJson és la teva funció de 07-03, que ja comprova response.ok i llança ErrorDeApi. Reutilitzar-la dins de React sense adaptador és la millor demostració que la lògica que no depèn del framework sobreviu al framework.

Solució 3

// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';

/**
 * Retorna `valor` amb un retard: només s'actualitza quan `valor` deixa
 * de canviar durant `retard` mil·lisegons.
 */
export function useDebounce(valor, retard = 300) {
  const [retardat, setRetardat] = useState(valor);

  useEffect(() => {
    const id = setTimeout(() => setRetardat(valor), retard);
    return () => clearTimeout(id);      // ← imprescindible
  }, [valor, retard]);

  return retardat;
}
// src/components/CercadorTasques.jsx
import { useState, useMemo } from 'react';
import { useDebounce } from '../hooks/useDebounce.js';
import { LlistaTasques } from './LlistaTasques.jsx';

export function CercadorTasques({ tasques, enAvancar }) {
  const [text, setText] = useState('');
  const textRetardat = useDebounce(text, 300);

  const visibles = useMemo(() => {
    const t = textRetardat.trim().toLowerCase();
    return t === '' ? tasques : tasques.filter((x) => x.titol.toLowerCase().includes(t));
  }, [tasques, textRetardat]);

  return (
    <>
      <label htmlFor="cercar">Cercar tasques</label>
      <input
        id="cercar"
        type="search"
        value={text}                                 // ← valor IMMEDIAT
        onChange={(e) => setText(e.target.value)}
      />
      <LlistaTasques tasques={visibles} enAvancar={enAvancar} />
    </>
  );
}

Punt 3 · Per què l'<input> fa servir el valor immediat. Un camp controlat mostra exactament el que diu el seu value. Si li passessis textRetardat, les lletres apareixerien 300 ms després de prémer-les: la sensació seria la d'un teclat espatllat, i esborrar de pressa produiria salts del cursor. És exactament el problema que 09-02 descrivia en distingir entre el que es veu (ha de ser immediat) i el que costa (es pot retardar). El filtre, que recorre les tasques, és el que es retarda.

Punt 4 · Per què és imprescindible la neteja. Sense clearTimeout, cada pulsació programaria un temporitzador nou sense cancel·lar l'anterior. Escriure «serigrafia» (10 lletres) deixaria deu temporitzadors vius, i els deu es dispararien: el filtre s'executaria deu vegades amb deu valors diferents, en ordre, i l'efecte de retard desapareixeria per complet. A més, si el component es desmunta mentre hi ha temporitzadors pendents, es cridaria setRetardat sobre un component mort. És, punt per punt, el problema 4 de 10-01 i la raó del teu destruir() de 09-03 — només que aquí React garanteix la crida.

Conclusió

Has vist React sencer en la seva versió moderna i, més important, has vist quin problema resol cada peça, perquè ja havies resolt tots aquests problemes a mà.

Saps que JSX no és HTML sinó sucre sintàctic que es compila a crides de funció que retornen objectes descriptors, i que d'aquí surten totes les seves regles: un sol element arrel perquè return retorna un valor, expressions i no sentències entre claus, className perquè class és reservada, valors que no es pinten —amb el parany del 0 a &&— i escapament automàtic que és el teu textContent de sempre. Saps que un component és una funció pura de props a descripció, amb la majúscula inicial com a requisit del compilador, i que es compon passant dades, funcions i children.

Has tancat el cercle de la key: React aparella els elements d'una llista per clau i, sense ella, per posició — amb el resultat exacte que vas analitzar a 06-06 quan reconciliar no feia servir data-id: cinc nodes que canvien de dada, el focus perdut i l'estat intern atribuït a la tasca equivocada. La key de React és el teu data-id, i les tres regles són les mateixes: estable, única entre germanes i mai l'índex si la llista es mou.

Saps com funciona useState: el valor inicial només compta un cop —amb la forma mandrosa per a càlculs cars—, la variable no canvia en l'execució actual perquè és un closure de 03-04, les actualitzacions s'agrupen, la forma de funció encadena, i la comparació per referència amb Object.is és la raó exacta per la qual l'estat es tracta com a immutable, amb el { ...tasca, estat: 'feta' } de 04-07 i el map que conserva les referències del que no canvia. I saps que la forma de l'estat importa més que l'estat: no guardis el que pots calcular, guarda identificadors en comptes d'objectes, i modela els estats de manera que els impossibles no es puguin expressar.

Entens useEffect amb precisió: serveix per sincronitzar amb sistemes externs a React, i no per derivar estat, ni per reaccionar a clics, ni per transformar dades. Coneixes l'array de dependències amb la seva comparació per referència i el parany dels objectes literals, i saps que la funció de neteja és el teu destruir(), amb el contingut idèntic i la diferència decisiva que la crida està garantida. Saps per què derivar estat amb efectes produeix dos renderitzats i dues fonts de veritat, i per què això reintrodueix dins del framework justament el problema que el framework venia a resoldre.

Distingeixes useMemo, useCallback i useRef —resultat, funció i caixa mutable—, saps que useMemo és la teva memòria cau per versió de 09-02 amb les dependències fent de comptador, i tens l'avís de 09-01 amb noms i procediment: memoïtzar no és gratis, useCallback sense React.memo al destinatari no serveix, i el camí correcte és Profiler → component car → prop que canvia → optimització mínima → mesurar un altre cop. Saps extreure hooks personalitzatsuseTauler i useFiltreResponsable— i que un hook comparteix lògica però no estat, amb les dues regles dels hooks i el seu motiu real: React identifica l'estat per ordre de crida.

Coneixes la diferència entre formularis controlats i no controlats, amb el criteri per triar i el recordatori que FormData, Object.fromEntries i la validació nativa de 06-07 continuen sent la millor eina per a una alta senzilla. Entens el DOM virtual amb les seves tres regles heurístiques i el seu intercanvi explícit —més feina en JavaScript, menys al DOM, amb cost proporcional a quant n'hi ha i no a quant canvia—, i saps que existeixen el compilador de React i els components de servidor, i quin problema ataca cadascun. I saps carregar dades amb els seus tres problemes reals —condició de cursa, manca de cancel·lació i doble execució en mode estricte— resolts amb l'AbortController de 07-03 i un estat amb fase, a més de per què en producció es fa servir una memòria cau d'estat de servidor com TanStack Query.

Finalment, has vist la pantalla objectiu escrita en React: TargetaTasca, FiltreResponsable, LlistaTasques, dos hooks i un fitxer de domini en JavaScript pur que no canviaria en canviar de framework. Amb la comparació honesta davant de tauler-vista.js: unes 130 línies davant de 210, zero línies d'infraestructura davant de les 60 de reconciliar, $, $$ i la delegació — a canvi de 45 kB de motor, un pas de compilació obligatori, conceptes nous per depurar i menys control directe sobre el DOM. Per a sis tasques i una persona, no compensa. Per a cinc pantalles i quatre persones, compensa.

Hi ha un problema que aquesta lliçó ha deixat deliberadament fora. useFiltreResponsable funciona perquè el filtre viu a App i des d'allà baixa als dos llocs que el necessiten. Què passa quan també el necessiten l'encaminador, el resum de la capçalera, l'informe i un component enterrat a sis nivells de profunditat? Passar la prop per sis components que no la fan servir té nom —prop drilling— i és el punt de partida de la lliçó següent: Gestió d'Estat amb Redux.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats