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
- Què és React i què no és
- Posar en marxa un projecte
- JSX: què és i en què es converteix
- Components de funció
propsi composició- Renderitzat de llistes i la
key key: el mateix problema que vas resoldre a 06-06- Renderitzat condicional
- Estat amb
useState - Per què l'estat es tracta com a immutable
- La forma de l'estat importa més que l'estat
- Esdeveniments a React i la diferència amb
addEventListener useEffect: què és i què no és- L'array de dependències
- La funció de neteja: el teu
destruir() - L'error de derivar estat amb efectes
useMemo,useCallbackiuseRef- No optimitzis sense mesurar
- Hooks personalitzats:
useTauleriuseFiltreResponsable - Les regles dels hooks i per què existeixen
- Formularis controlats i no controlats
- El DOM virtual i la reconciliació, de debò
- El compilador de React i els components de servidor
- Carregar dades amb
useEffecti els seus tres problemes - Per què en producció es fa servir una llibreria de dades
- L'ecosistema mínim
- Nómada Tasques en React: la llista completa
- Comparació costat a costat amb
tauler-vista.js - Errors Habituals i Consells
- Exercicis
- Conclusió
- 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.
- Posar en marxa un projecte
Amb Vite, que ja fas servir des de 09-05, un projecte React nou es crea així:
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.
- JSX: què és i en què es converteix
Això és JSX:
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 <>…</>:
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 sí 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.
- 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:
- 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. - 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
fd'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.
props i composició
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.
- Renderitzat de llistes i la
key
keyUna 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.
key: el mateix problema que vas resoldre a 06-06
key: el mateix problema que vas resoldre a 06-06Aquest é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ó.
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.
- 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:
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ò:
É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.
- Estat amb
useState
useStateFins 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:
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 renderAquesta 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 24 · 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.
- 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 renderitzaLa 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:
mapretorna 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 elspreadde 04-07, exactament.
Aquí hi ha una tensió real amb Nómada Tasques que convé anomenar. La teva classe Tasca té #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.
- 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.
- Esdeveniments a React i la diferència amb
addEventListener
addEventListenerA React els gestors es declaren com a props:
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.
useEffect: què és i què no és
useEffect: què és i què no ésuseEffect é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.
useEffectserveix 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.
- 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 filtreLa 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:
- Dependre de valors primitius:
[filtres.responsable, filtres.text]en lloc de[filtres]. Gairebé sempre és el correcte. - Moure la creació de l'objecte fora del component si és constant.
- Memoïtzar l'objecte amb
useMemoal 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.
- La funció de neteja: el teu
destruir()
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ó».
- 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:
- 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.
- Hi ha dues fonts de veritat.
tasquesihoresOberteses poden desincronitzar: és el problema 1 de 10-01 reinventat dins del framework que venia a resoldre'l. - É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.
useMemo, useCallback i useRef
useMemo, useCallback i useRefTres 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 foraFixa'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).
- 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:
- 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.
- Troba el component car. Gairebé sempre n'hi ha un: una llista llarga, un gràfic, un càlcul pesat.
- 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
useCallbackserveix. - 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.
- Hooks personalitzats:
useTauler i useFiltreResponsable
useTauler i useFiltreResponsableUn 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:
- 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. - Les regles de negoci són fora.
SEGUENTi les R1–R10 viuen adomini/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. - Cada crida al hook crea el seu propi estat. Dos components que facin servir
useFiltreResponsabletenen dos filtres independents. Un hook no comparteix estat: comparteix lògica. Confondre això és el malentès número u sobre els hooks. useMemoaquí sí que té un motiu, i no és el rendiment del càlcul: és mantenir estable la referència devisiblesiresponsablesperquè els components fills memoïtzats no es repintin sense motiu.new Set(...)de 04-05 elimina duplicats ifilter(Boolean)descarta elsnullde la R8: dues utilitats de JavaScript pur que es fan servir igual dins de React.
- Les regles dels hooks i per què existeixen
Dues regles, i una explicació que gairebé mai no es dona:
- Només es criden al nivell superior d'un component o d'un altre hook. Mai dins d'un
if, un bucle, untryo una funció imbricada. - 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.
- 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.
- 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.
- 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.
- Carregar dades amb
useEffect i els seus tres problemes
useEffect i els seus tres problemesAquest é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'han pogut carregar: {estat.error.message}</p>;
return <LlistaTasques tasques={estat.tasques} />;
}Les tres correccions, una per problema:
- 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
thenperquè la seva promesa es rebutja ambAbortError. - La cancel·lació la dona el mateix
AbortControllerde 07-03, passat ademanarJsoncom asignal. - 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.
- 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.
- 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.
- 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:
memofa que la targeta només es torni a renderitzar si les seves props canvien per referència. Aquí sí que té sentit, perquèuseTaulerretorna el mateix objecte tasca per a les que no canvien (gràcies almapimmutable de l'apartat 19) ienAvancarés estable gràcies auseCallback. Sense aquestes dues condicions,memono serviria de res.data-ides conserva. React no el necessita —per a això hi ha lakey—, 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]ésnull, 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.
- Comparació costat a costat amb
tauler-vista.js
tauler-vista.jsAra 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
reconciliares 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
requestAnimationFrameara s'han de fer a través de React, ambrefi 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.groupByoreduce, com a 04-05. - El component rep les tasques per props i no té estat propi.
- Afegeix una
keycorrecta 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:
- Escriu un hook
useDebounce(valor, retard)que retorni el valor amb retard, fent serviruseStateiuseEffectamb la seva neteja. - Fes-lo servir en un component
<CercadorTasques>amb un<input>controlat que filtri la llista per títol. - 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. - 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 personalitzats —useTauler 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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
