El mòdul anterior va acabar muntant el mapa dels hooks; ara comença el recorregut pel territori, i el primer punt del mapa és el que ja fas servir des de 02-04: useState. Allà vas aprendre l'essencial —declarar estat, actualitzar-lo, no mutar-lo— i amb això el catàleg de CicloUrbano funciona. Però useState amaga un comportament que sorprèn gairebé tothom la primera vegada que el troba: durant un render, la variable d'estat no canvia mai, per moltes vegades que cridis l'actualitzador. Qui no ho entén acaba escrivint codi que «perd» actualitzacions sense saber per què, i desconfiant de React. En aquesta lliçó entendràs el mecanisme real: l'estat com a instantània, la cua d'actualitzacions, l'agrupació de canvis, la inicialització mandrosa, un receptari complet d'actualització immutable aplicat a les reserves de CicloUrbano, cinc principis per estructurar bé l'estat i el truc de reiniciar-lo amb la prop key.
Contingut
- La signatura de
useStatei el parell que retorna - Per què l'actualitzador és estable entre renders
- L'estat és una instantània del render
- Tres
setComptador(comptador + 1)que sumen només un - La forma funcional i la cua d'actualitzacions
- Agrupació d'actualitzacions (batching)
- Inicialització mandrosa i la trampa del càlcul directe
- Objectes i arrays: receptari d'actualització immutable
- Estructurar bé l'estat: cinc principis
- Reiniciar l'estat d'un component amb la prop
key - Valor directe o funció actualitzadora: taula de decisió
- La signatura de
useState i el parell que retorna
useState i el parell que retornauseState rep un argument —el valor inicial— i sempre retorna un array de exactament dues posicions:
| Posició | Què és | Com s'utilitza |
|---|---|---|
[0] |
El valor de l'estat en aquest render | Es llegeix. Mai s'assigna |
[1] |
La funció actualitzadora | Es crida per demanar un canvi i un nou render |
Que retorni un array i no un objecte no és un caprici: en desestructurar un array tries tu els noms, posició a posició. Si retornés { valor, establirValor } hauries de renombrar a cada crida, i amb tres estats al mateix component el codi seria il·legible.
// Amb array (el que fa React): noms lliures, una línia
const [tipusElegit, setTipusElegit] = useState('todos');
const [horesReserva, setHoresReserva] = useState(2);
// Si retornés un objecte: renombrat obligatori a cada ús
const { valor: tipusElegit, establir: setTipusElegit } = useState('todos');La convenció universal a l'ecosistema és [algo, setAlgo]. En aquest curs la respectem per a l'actualitzador encara que la resta de noms estigui en català, perquè set forma part de l'idioma de React tant com useState.
L'argument només s'utilitza en el primer render. A partir d'aquí React ignora el que li passis: el valor inicial és exactament això, inicial. Aquesta línia no «reinicia» res en el render número 40:
- Per què l'actualitzador és estable entre renders
Aquesta és una propietat que es formula poc i que resulta molt útil:
React garanteix que la funció actualitzadora que retorna
useStateés la mateixa referència en tots els renders del component. No es recrea mai.
Comprova-ho amb una comparació d'identitat:
let actualitzadorAnterior = null;
function ComptadorPlaces({ estacio }) {
const [ocupades, setOcupades] = useState(0);
console.log('Mateix actualitzador que el render anterior?', setOcupades === actualitzadorAnterior);
actualitzadorAnterior = setOcupades;
// render 1: false (no hi havia anterior) · render 2, 3, 4...: true, sempreTres conseqüències pràctiques que aprofitaràs en les properes lliçons:
- No cal incloure'l a les dependències d'un efecte (05-02). El linter ho sap i no t'ho exigeix.
- No cal embolcallar-lo en
useCallback(08-03) per passar-lo a un fill memoitzat: ja és estable per definició. - Es pot passar directament com a prop de callback sense por de provocar renders innecessaris.
// Correcte i suficient: setOcupades no canvia mai
useEffect(() => {
const id = setInterval(() => setOcupades((previes) => previes + 1), 5000);
return () => clearInterval(id);
}, []); // sense setOcupades a les dependències: React garanteix que és estableLa mateixa garantia s'aplica a la funció despatxar de useReducer, com veuràs a 05-05.
- L'estat és una instantània del render
Aquí hi ha la idea central de la lliçó, i convé formular-la a poc a poc.
Cada render és una fotografia congelada. Les variables d'estat d'aquest render són constants: valen el que valien quan React va cridar la funció del component, i no canvien fins que comença el render següent.
Recorda com funciona un component de funció: és una funció que React crida. A cada render, React la torna a executar i useState retorna el valor que correspon a aquest render. La variable horesReserva no és una capsa màgica connectada a React: és una const local d'aquesta execució concreta.
function PanellReserva({ bicicleta, hores, alCanviarHores }) {
// 'hores' és una constant D'AQUEST RENDER. Res la canviarà durant aquesta execució.
console.log('Render amb hores =', hores);
...
}L'exemple que ho fa evident és un gestor amb un retard:
function SelectorHores() {
const [hores, setHores] = useState(2);
function gestionarReservaDiferida() {
setHores(hores + 3); // demanem passar a 5
setTimeout(() => {
alert(`Reservant ${hores} hores`); // què mostra: 2 o 5?
}, 3000);
}
return (
<>
<p>Hores: {hores}</p>
<button type="button" onClick={gestionarReservaDiferida}>Reservar en 3 s</button>
</>
);
}Mostra 2. I no és un error: és exactament el correcte. El gestor gestionarReservaDiferida es va crear durant el render en què hores valia 2, i aquesta funció captura aquest valor. Encara que tres segons després la pantalla ja mostri 5, la funció continua vivint a la fotografia del render antic. És el comportament normal dels tancaments (closures) de JavaScript, aplicat a React.
Pensa-ho així: si prems «enviar» en un formulari i després, mentre s'envia, canvies un camp, el missatge enviat ha de ser el que hi havia en prémer, no el que hi ha ara. La instantània no és un obstacle: és el que fa que els esdeveniments siguin predictibles.
- Tres
setComptador(comptador + 1) que sumen només un
setComptador(comptador + 1) que sumen només unAquest és l'exemple clàssic, i ara ja tens les eines per explicar-lo. Afegim a CicloUrbano un botó que suma tres places de cop:
// src/components/ComptadorPlaces.jsx (versió amb la fallada)
import { useState } from 'react';
function ComptadorPlaces() {
const [placesLliures, setPlacesLliures] = useState(0);
function gestionarAlliberarTres() {
setPlacesLliures(placesLliures + 1);
setPlacesLliures(placesLliures + 1);
setPlacesLliures(placesLliures + 1);
}
return (
<>
<p>Places lliures: {placesLliures}</p>
<button type="button" onClick={gestionarAlliberarTres}>Alliberar 3 places</button>
</>
);
}Prems una vegada i el comptador marca 1, no 3. Substitueix mentalment la variable pel seu valor en aquest render i veuràs per què:
// Durant aquest render, placesLliures val 0. Les tres línies són, literalment:
setPlacesLliures(0 + 1); // encua: "el pròxim estat és 1"
setPlacesLliures(0 + 1); // encua: "el pròxim estat és 1"
setPlacesLliures(0 + 1); // encua: "el pròxim estat és 1"Les tres crides encuen la mateixa ordre: «posa l'estat a 1». React processa la cua, obté 1, i renderitza una vegada. No s'ha perdut cap actualització: és que les tres deien el mateix.
La conclusió que cal interioritzar: cridar l'actualitzador no canvia la variable en curs. setPlacesLliures(1) no fa placesLliures = 1; fa «apunta que en el pròxim render valdrà 1».
- La forma funcional i la cua d'actualitzacions
La solució és passar una funció actualitzadora en lloc d'un valor. React la desa a la cua i l'executa després, una darrere l'altra, passant-li el resultat de l'anterior.
function gestionarAlliberarTres() {
setPlacesLliures((previes) => previes + 1);
setPlacesLliures((previes) => previes + 1);
setPlacesLliures((previes) => previes + 1);
}Ara sí: 3. Així processa React la cua abans del render següent:
flowchart LR
A["Estat actual: 0"] --> B["f1: previes => previes + 1<br/>0 → 1"]
B --> C["f2: previes => previes + 1<br/>1 → 2"]
C --> D["f3: previes => previes + 1<br/>2 → 3"]
D --> E["Estat del pròxim render: 3"]
style A fill:#e0f2fe
style E fill:#dcfce7
I aquest és el contrast amb la versió de valor directe, on cada element de la cua és un valor fix que substitueix el que hi hagués:
| Cua encuada | Com la processa React | Estat final |
|---|---|---|
1, 1, 1 |
Substituir, substituir, substituir | 1 |
f, f, f amb f = p => p + 1 |
f(0)=1, f(1)=2, f(2)=3 |
3 |
5, f, f |
Substituir per 5, f(5)=6, f(6)=7 |
7 |
La tercera fila ensenya que es poden combinar: un valor directe descarta l'anterior i reinicia el càlcul. És útil, per exemple, per «reiniciar i sumar»:
setPlacesLliures(0); // reinicia
setPlacesLliures((previes) => previes + estacio.places); // suma sobre el 0 acabat de posarRegles per escriure bones funcions actualitzadores:
- Han de ser pures: calcular i retornar el nou estat, sense
console.logque depenguin de l'ordre, sense peticions, sense mutar res. React pot cridar-les dues vegades en desenvolupament ambStrictMode(01-03) precisament per detectar impureses. - Anomena el paràmetre amb claredat:
previes,reservesPrevies,dadesPrevies. Evita la lletra solta. - No llegeixis altres variables d'estat a dins: si el càlcul depèn de dos estats alhora, és senyal que aquests dos estats haurien de ser un de sol, o que ha arribat el moment de
useReducer(05-05).
- Agrupació d'actualitzacions (batching)
React no repinta després de cada crida a l'actualitzador: agrupa totes les actualitzacions que es produeixen i fa un únic render al final. És el que s'anomena batching.
function gestionarConfirmacio({ bicicleta, hores }) {
setBicicletaSeleccionada(null); // 1
setHoresReserva(2); // 2
setAvisVisible(true); // 3
// React ENCARA no ha renderitzat. Renderitza UNA vegada quan acaba aquest gestor.
}Tres canvis d'estat, un sol render. Sense agrupació tindries tres renders i estats intermedis visibles en pantalla: la interfície mostraria per un instant el panell buit amb les hores antigues. L'agrupació evita aquests estats a mig fer.
El canvi important de React 18 en endavant: l'agrupació és automàtica a tot arreu. Abans només agrupava dins de gestors d'esdeveniments de React; el codi dins de promeses, temporitzadors o gestors natius provocava un render per crida.
| Context | React 17 | React 18 i 19 |
|---|---|---|
Gestor onClick de React |
Agrupa (1 render) | Agrupa (1 render) |
Dins de setTimeout |
No agrupa (3 renders) | Agrupa (1 render) |
Dins de .then() d'una promesa |
No agrupa (3 renders) | Agrupa (1 render) |
Dins d'un addEventListener natiu |
No agrupa (3 renders) | Agrupa (1 render) |
// A React 19 això provoca UN sol render, no tres
async function gestionarCarregaEstacions() {
const resposta = await fetch('/api/estaciones');
const dades = await resposta.json();
setEstacions(dades); // ┐
setCarregant(false); // ├─ un únic render al final
setError(null); // ┘
}Si alguna vegada necessites forçar un repintat immediat abans de continuar —un cas molt rar, típicament per mesurar el DOM—, existeix flushSync de react-dom. Considera-ho una vàlvula d'escapament: el seu ús habitual és un símptoma que alguna cosa està mal plantejada.
- Inicialització mandrosa i la trampa del càlcul directe
L'argument de useState només s'utilitza en el primer render, però s'avalua en tots. Aquesta diferència té conseqüències.
// ❌ MALAMENT: calcularResumFlota(bicicletes) s'EXECUTA a cada render
const [resum, setResum] = useState(calcularResumFlota(bicicletes));Encara que React descarti el resultat a partir del segon render, JavaScript ha hagut d'executar la funció per poder passar el seu valor com a argument. Si recorre cinc bicicletes és igual; si llegeix localStorage, parseja un JSON gran o recorre milers de reserves, estàs pagant aquest cost a cada repintat.
La solució és passar la funció, sense cridar-la. React l'executarà una única vegada, a la inicialització:
// ✅ BÉ: React crida la funció només en el primer render
const [resum, setResum] = useState(() => calcularResumFlota(bicicletes));Un cas molt habitual a CicloUrbano: recuperar l'esborrany de reserva desat al navegador.
function FormulariReserva({ alCrearReserva }) {
const [dades, setDades] = useState(() => {
const desat = localStorage.getItem('ciclourbano:borrador');
return desat ? JSON.parse(desat) : DADES_INICIALS;
});
...
}Sense la funció embolcalladora, cada pulsació de tecla al formulari provocaria un render, i cada render llegiria i parsejaria localStorage per llençar el resultat a les escombraries.
| Forma | Quan s'executa el càlcul | Quan usar-la |
|---|---|---|
useState(valor) |
Mai hi ha càlcul: és un literal | Valors simples: 0, '', false, 'todos', null |
useState(calcular()) |
A cada render | Mai, si calcular no és trivial |
useState(() => calcular()) |
Només en el primer render | Càlculs cars, lectura de localStorage, parseig de JSON |
Compte amb l'ambigüitat si l'estat és una funció. Si vols desar una funció com a valor d'estat, useState(laMevaFuncio) la interpretaria com a inicialitzador mandrós i desaria el seu resultat. Cal embolcallar-la: useState(() => laMevaFuncio).
- Objectes i arrays: receptari d'actualització immutable
A 02-04 va quedar establerta la regla: l'estat se substitueix, no es modifica. React compara la referència anterior amb la nova i, si són la mateixa, considera que no ha canviat res i no renderitza. Aquí tens el receptari complet, aplicat a la llista de reserves de CicloUrbano.
Partim d'aquest estat a App:
// src/dades/domini.js
export const reserves = [
{
id: 'res-01',
bicicletaId: 'bici-002',
usuari: 'usr-01',
dataInici: '2026-05-04T09:00',
hores: 2,
estat: 'activa'
}
];Receptari d'arrays
| Operació | ❌ Muta (malament) | ✅ Substitueix (bé) |
|---|---|---|
| Afegir al final | arr.push(x) |
setArr([...arr, x]) |
| Afegir al principi | arr.unshift(x) |
setArr([x, ...arr]) |
| Eliminar per id | arr.splice(i, 1) |
setArr(arr.filter((e) => e.id !== id)) |
| Substituir un element | arr[i] = x |
setArr(arr.map((e) => (e.id === id ? x : e))) |
| Ordenar | arr.sort(f) |
setArr([...arr].sort(f)) |
| Invertir | arr.reverse() |
setArr([...arr].reverse()) |
| Inserir en una posició | arr.splice(i, 0, x) |
setArr([...arr.slice(0, i), x, ...arr.slice(i)]) |
| Buidar | arr.length = 0 |
setArr([]) |
Els quatre casos que més apareixen, escrits amb la forma funcional (perquè depenen del valor anterior):
// AFEGIR una reserva nova
function gestionarCrearReserva(novaReserva) {
setLlistaReserves((previes) => [...previes, novaReserva]);
}
// ELIMINAR (cancel·lar definitivament) per identificador
function gestionarEliminarReserva(idReserva) {
setLlistaReserves((previes) => previes.filter((reserva) => reserva.id !== idReserva));
}
// SUBSTITUIR una reserva sencera
function gestionarSubstituirReserva(reservaActualitzada) {
setLlistaReserves((previes) =>
previes.map((reserva) => (reserva.id === reservaActualitzada.id ? reservaActualitzada : reserva))
);
}
// CANVIAR UNA PROPIETAT d'un element (sense tocar la resta)
function gestionarCancellarReserva(idReserva) {
setLlistaReserves((previes) =>
previes.map((reserva) =>
reserva.id === idReserva ? { ...reserva, estat: 'cancelada' } : reserva
)
);
}Fixa't en gestionarCancellarReserva: el map retorna el mateix objecte per a les reserves que no canvien i un objecte nou només per a l'afectada. Això és exactament el que volem: la mínima quantitat de referències noves, perquè les optimitzacions del Mòdul 8 puguin saltar-se la resta.
Avís sobre sort i reverse: són mètodes que modifiquen l'array original. [...arr].sort(f) copia primer. El JavaScript modern ofereix a més toSorted() i toReversed(), que ja retornen una còpia i fan innecessari el [...arr].
Receptari d'objectes i imbricació
const [esborrany, setEsborrany] = useState({
bicicletaId: '',
dataInici: '',
hores: 2,
condicions: false,
contacte: { email: '', telefon: '' } // ← nivell imbricat
});| Operació | ✅ Com es fa |
|---|---|
| Canviar una propietat | setEsborrany({ ...esborrany, hores: 4 }) |
| Canviar una propietat dinàmica | setEsborrany({ ...esborrany, [camp]: valor }) |
| Canviar una propietat imbricada | setEsborrany({ ...esborrany, contacte: { ...esborrany.contacte, email } }) |
| Eliminar una propietat | const { hores, ...resta } = esborrany; setEsborrany(resta); |
El cas imbricat és el que produeix més errors, perquè cal copiar tots els nivells del camí fins a la propietat que canvia:
function gestionarCanviEmail(email) {
setEsborrany((previ) => ({
...previ, // copia el nivell 1
contacte: { ...previ.contacte, email } // copia el nivell 2 i canvia l'email
}));
}Això oblida copiar el nivell intermedi i esborra el telèfon:
// ❌ contacte passa a ser { email } i el telèfon desapareix
setEsborrany((previ) => ({ ...previ, contacte: { email } }));I aquest altre sembla funcionar però és una mutació disfressada: modifica l'objecte que ja hi havia a l'estat, de manera que la referència de esborrany no canvia i React pot no renderitzar:
// ❌ mutació: previ.contacte és el MATEIX objecte que hi ha a l'estat
setEsborrany((previ) => {
previ.contacte.email = email;
return { ...previ };
});Tingues en compte també que els parèntesis al voltant de l'objecte a (previ) => ({ ... }) són obligatoris: sense ells, JavaScript interpreta les claus com el cos de la funció i la fletxa retorna undefined.
Si la imbricació passa de dos nivells, la resposta correcta gairebé mai és escriure spreads més llargs: és aplanar l'estat (apartat següent) o recórrer a una biblioteca d'immutabilitat com Immer, que permet escriure codi d'aspecte mutable i produeix còpies per sota.
- Estructurar bé l'estat: cinc principis
Triar què desar a l'estat importa més que saber actualitzar-lo. Aquests cinc principis eviten la majoria dels errors d'estat que veuràs en producció.
- Agrupa el que canvia junt
Si dues variables s'actualitzen sempre alhora, són una de sola.
// ❌ dos estats que mai canvien per separat
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// ✅ un de sol
const [posicio, setPosicio] = useState({ x: 0, y: 0 });
- No duplicis la mateixa dada
És la regla de la font única de veritat de 04-01, ara dins d'un mateix component.
// ❌ la bicicleta hi és dues vegades: si canvia la llista, la còpia queda obsoleta
const [bicicletes, setBicicletes] = useState(dadesInicials);
const [bicicletaSeleccionada, setBicicletaSeleccionada] = useState(null);
// ✅ desa l'identificador i busca l'objecte en renderitzar
const [idSeleccionada, setIdSeleccionada] = useState(null);
const bicicletaSeleccionada = bicicletes.find((b) => b.id === idSeleccionada) ?? null;La versió correcta té un benefici immediat: si l'operari canvia l'estat de bici-002 a «manteniment», la targeta seleccionada se n'assabenta. Amb la còpia, continuaria mostrant les dades velles.
- Evita l'estat contradictori
Tres booleans solts permeten combinacions que no haurien d'existir.
// ❌ què significa carregant=true i error='Fallada'? És un estat impossible
const [carregant, setCarregant] = useState(false);
const [error, setError] = useState(null);
const [dades, setDades] = useState(null);Amb aquesta estructura hi ha 2 × 2 × 2 = 8 combinacions i només 4 tenen sentit. N'hi ha prou d'oblidar un setCarregant(false) en una branca d'error perquè la interfície mostri alhora el missatge de fallada i l'indicador de càrrega.
// ✅ una sola variable amb els estats que EXISTEIXEN de veritat
const [estatCarrega, setEstatCarrega] = useState('inactiu');
// 'inactiu' | 'carregant' | 'exit' | 'error'
const [dades, setDades] = useState(null);
const [error, setError] = useState(null);
const estaCarregant = estatCarrega === 'carregant'; // derivat, no estat
- Evita l'estat redundant que pots derivar
Si una dada es pot calcular a partir d'altres, no és estat. Ja ho vas veure a 04-01 amb bicicletesVisibles; l'error més comú és el «total» calculat:
// ❌ redundant: cal recordar de recalcular-lo a cada canvi
const [hores, setHores] = useState(2);
const [total, setTotal] = useState(5);
// ✅ derivat: sempre correcte, impossible de desincronitzar
const [hores, setHores] = useState(2);
const total = hores * bicicleta.preuHora;
- Aplana l'imbricat
Desar la jerarquia tal com arriba del servidor obliga a spreads de tres nivells per canviar una dada. Desar índexs per identificador converteix aquesta operació en una línia.
// ❌ profundament imbricat: canviar una bici obliga a copiar estació i llista
const [xarxa, setXarxa] = useState({
estacions: [{ id: 'est-01', nom: 'Plaça Major', bicicletes: [{ id: 'bici-001', ... }] }]
});
// ✅ aplanat per identificador
const [estacionsPerId, setEstacionsPerId] = useState({
'est-01': { id: 'est-01', nom: 'Plaça Major', barri: 'Centre', places: 20, bicicletaIds: ['bici-001', 'bici-002'] }
});
const [bicicletesPerId, setBicicletesPerId] = useState({
'bici-001': { id: 'bici-001', model: 'Urbana Clàssica', tipus: 'urbana', estat: 'disponible', estacioId: 'est-01', preuHora: 2.5 }
});
// Canviar l'estat d'una bicicleta: una línia
setBicicletesPerId((previes) => ({
...previes,
'bici-001': { ...previes['bici-001'], estat: 'mantenimiento' }
}));Quan aquests cinc principis es queden curts —moltes peces relacionades, transicions amb regles, gestors que toquen tres estats alhora— la resposta ja no és reorganitzar useState, sinó canviar d'eina: useReducer reuneix totes aquestes peces sota una funció de transició única. És la lliçó 05-05.
- Reiniciar l'estat d'un component amb la prop
key
keyReact conserva l'estat d'un component mentre es renderitzi a la mateixa posició de l'arbre amb el mateix tipus. Això produeix un comportament que sorprèn: si canvies de bicicleta seleccionada, el PanellReserva manté les hores que hi havia posades per a l'anterior.
<PanellReserva bicicleta={bicicletaSeleccionada} />
// Canvia de bici-001 a bici-005 → mateix component, mateixa posició → l'estat intern sobreviuPodries «arreglar-ho» amb un efecte que reiniciï l'estat en canviar la prop, però és la pitjor solució i la veurem com a antipatró a 05-02. La solució idiomàtica és una key diferent:
<PanellReserva
key={bicicletaSeleccionada?.id}
bicicleta={bicicletaSeleccionada}
hores={horesReserva}
alCanviarHores={gestionarCanviHores}
alConfirmar={gestionarConfirmacio}
/>En canviar la key, React considera que és un altre component diferent: desmunta l'anterior amb tot el seu estat i en munta un de nou des de zero. És la mateixa key que fas servir a les llistes (03-03), aplicada aquí amb un altre propòsit: no identificar germans, sinó declarar identitat al llarg del temps.
flowchart TD
A["bicicletaSeleccionada = bici-001"] --> B["PanellReserva key='bici-001'<br/>hores: 4"]
B --> C{"L'usuari tria bici-005"}
C --> D["React veu una key diferent"]
D --> E["Desmunta el panell anterior<br/>(el seu estat es perd)"]
E --> F["Munta PanellReserva key='bici-005'<br/>hores: 1 (valor inicial)"]
style E fill:#fecaca
style F fill:#dcfce7
Casos típics on la key és l'eina correcta:
- Un formulari que ha de buidar-se en canviar d'element editat.
- Un panell de detall que ha de tornar a la seva pestanya inicial en canviar de registre.
- Un component de tercers que no ofereix cap manera de reiniciar-se.
- Valor directe o funció actualitzadora: taula de decisió
| Situació | Forma recomanada | Exemple |
|---|---|---|
| El nou valor no depèn de l'anterior | Valor directe | setTipusElegit('electrica') |
| El nou valor sí que depèn de l'anterior | Funció actualitzadora | setHores((previes) => previes + 1) |
| Diverses actualitzacions seguides del mateix estat | Funció actualitzadora, sempre | setPlaces((p) => p + 1) × 3 |
Dins d'un setTimeout, fetch o await |
Funció actualitzadora | setComptador((p) => p + 1) després de 5 s |
| Dins d'un efecte amb dependències reduïdes | Funció actualitzadora | evita afegir l'estat a [deps] (05-02) |
| Afegir, treure o modificar en un array/objecte | Funció actualitzadora | setReserves((previes) => [...previes, nova]) |
| Posar un valor fix o reiniciar | Valor directe | setBicicletaSeleccionada(null) |
Regla pràctica per no dubtar: fes servir la forma funcional sempre que el nou estat es calculi a partir de l'actual. No penalitza el rendiment, no complica el codi i elimina d'arrel tota una família d'errors difícils de reproduir.
Errors Comuns i Consells
- Llegir l'estat just després d'actualitzar-lo.
setHores(5); console.log(hores);imprimeix el valor vell. És la instantània de l'apartat 3, no un retard. Si necessites el valor nou, calcula'l en una constant:const novesHores = 5; setHores(novesHores); console.log(novesHores);. - Cridar l'actualitzador durant el render.
setComptador(1)al cos del component provoca un bucle infinit («Too many re-renders»). Els actualitzadors es criden des de gestors o des d'efectes, mai durant el render. L'única excepció, poc freqüent, és l'ajust condicional d'estat amb unifque trenca el bucle. useState(calcular())en lloc deuseState(() => calcular()). El càlcul s'executa a tots els renders. Revisa aquesta línia sempre que l'inicialitzador no sigui un literal.- Mutar l'estat i renderitzar «a mà».
reserves.push(nova); setReserves(reserves);no repinta: la referència és la mateixa. I si a més hi ha memoització (Mòdul 8), el bug es torna intermitent. - Estats booleans que es contradiuen.
carregant,errorièxitcom a tres booleans independents acaben mostrant pantalles impossibles. Una cadena amb els estats vàlids ho resol. - Desar a l'estat l'objecte sencer quan n'hi ha prou amb l'id. És la duplicació del principi 2: la còpia envelleix malament.
- Oblidar els parèntesis a
(previ) => ({ ... }). Sense ells la funció no retorna res i l'estat passa aundefined. - Consell de depuració: quan un estat no s'actualitza com esperes, escriu la línia substituint mentalment cada variable pel seu valor en aquest render. La majoria dels misteris es resolen aquí.
- Consell de disseny: abans d'afegir un
useState, pregunta't «puc calcular-ho?». La major part de l'estat que sobra és estat derivat que algú no va voler derivar.
Exercicis
Exercici 1. Aquest component de CicloUrbano ha de sumar les hores de cop segons el botó que es premi, però en prémer «+3 hores» només en suma una. Corregeix-lo i explica per què fallava.
import { useState } from 'react';
function AjustadorHores() {
const [hores, setHores] = useState(1);
function gestionarSumarTres() {
setHores(hores + 1);
setHores(hores + 1);
setHores(hores + 1);
}
function gestionarReiniciarISumar() {
setHores(0);
setHores(hores + 2);
}
return (
<>
<p>Hores de la reserva: {hores}</p>
<button type="button" onClick={gestionarSumarTres}>+3 hores</button>
<button type="button" onClick={gestionarReiniciarISumar}>Reiniciar i +2</button>
</>
);
}Exercici 2. El panell de reserves de CicloUrbano desa el seu estat així. Reestructura'l aplicant els cinc principis de l'apartat 9 i escriu els gestors gestionarSeleccio, gestionarCanviHores i gestionarConfirmar amb l'estructura nova.
const [reserves, setReserves] = useState(reservesInicials);
const [reservaSeleccionada, setReservaSeleccionada] = useState(null); // objecte complet
const [hores, setHores] = useState(2);
const [preuTotal, setPreuTotal] = useState(0);
const [enviant, setEnviant] = useState(false);
const [enviat, setEnviat] = useState(false);
const [hiHaError, setHiHaError] = useState(false);
const [missatgeError, setMissatgeError] = useState('');Exercici 3. Escriu els tres gestors que gestionen la llista de reserves d'App, tots amb actualització immutable i forma funcional: gestionarConfirmar(idReserva) canvia l'estat d'aquesta reserva a 'confirmada'; gestionarCancellar(idReserva) el canvia a 'cancelada' i afegeix la propietat cancelladaEn amb la data ISO actual; gestionarPurgar() elimina de la llista totes les reserves l'estat de les quals sigui 'cancelada'. Parteix de l'estat const [llistaReserves, setLlistaReserves] = useState(reserves);.
Solucions
Solució 1.
function gestionarSumarTres() {
setHores((previes) => previes + 1);
setHores((previes) => previes + 1);
setHores((previes) => previes + 1);
}
function gestionarReiniciarISumar() {
setHores(0); // valor directe: descarta l'anterior
setHores((previes) => previes + 2); // funcional: opera sobre el 0 acabat d'encuar
}gestionarSumarTres fallava perquè hores és una constant d'aquest render. Amb hores = 1, les tres línies eren literalment setHores(2) tres vegades: la cua contenia 2, 2, 2 i el resultat era 2 (una sola hora sumada). Amb la forma funcional, la cua conté tres funcions que React aplica en cadena: 1→2→3→4.
A gestionarReiniciarISumar la barreja és intencionada. Amb la versió original (setHores(0); setHores(hores + 2);) i hores = 5, la cua seria 0 i després 7: el reinici es perd i el resultat és 7. Amb la versió corregida la cua és 0 i després f, així que f(0) = 2, que és el que promet el botó.
Solució 2.
// Vuit useState → tres, sense estats impossibles ni dades duplicades
const [llistaReserves, setLlistaReserves] = useState(reservesInicials);
const [idSeleccionada, setIdSeleccionada] = useState(null); // l'ID, no l'objecte (principi 2)
const [hores, setHores] = useState(2);
const [enviament, setEnviament] = useState({ estat: 'inactiu', error: null });
// estat: 'inactiu' | 'enviant' | 'enviat' | 'error' (principi 3)
// DERIVATS: no són estat (principi 4)
const reservaSeleccionada = llistaReserves.find((r) => r.id === idSeleccionada) ?? null;
const bicicleta = reservaSeleccionada
? bicicletes.find((b) => b.id === reservaSeleccionada.bicicletaId)
: null;
const preuTotal = bicicleta ? hores * bicicleta.preuHora : 0;
const estaEnviant = enviament.estat === 'enviant';
function gestionarSeleccio(idReserva) {
setIdSeleccionada(idReserva);
setEnviament({ estat: 'inactiu', error: null });
}
function gestionarCanviHores(novesHores) {
setHores(Math.max(1, novesHores));
}
async function gestionarConfirmar() {
setEnviament({ estat: 'enviant', error: null });
try {
await enviarReserva({ id: idSeleccionada, hores, total: preuTotal });
setLlistaReserves((previes) =>
previes.map((reserva) =>
reserva.id === idSeleccionada ? { ...reserva, hores, estat: 'confirmada' } : reserva
)
);
setEnviament({ estat: 'enviat', error: null });
} catch (error) {
setEnviament({ estat: 'error', error: error.message });
}
}Els canvis, principi a principi: reservaSeleccionada deixa de duplicar l'objecte i passa a derivar-se de l'id (2); preuTotal es calcula a cada render en lloc de desar-se (4); i els quatre booleans d'enviament es fonen en una sola variable amb quatre valors possibles, de manera que ja no existeix la combinació «enviant i amb error alhora» (3). A més, enviament agrupa estat i missatge, que sempre canvien junts (1). Amb setEnviament({ estat: 'error', error: error.message }) és impossible oblidar apagar l'indicador de càrrega.
Solució 3.
const [llistaReserves, setLlistaReserves] = useState(reserves);
function gestionarConfirmar(idReserva) {
setLlistaReserves((previes) =>
previes.map((reserva) =>
reserva.id === idReserva ? { ...reserva, estat: 'confirmada' } : reserva
)
);
}
function gestionarCancellar(idReserva) {
setLlistaReserves((previes) =>
previes.map((reserva) =>
reserva.id === idReserva
? { ...reserva, estat: 'cancelada', cancelladaEn: new Date().toISOString() }
: reserva
)
);
}
function gestionarPurgar() {
setLlistaReserves((previes) => previes.filter((reserva) => reserva.estat !== 'cancelada'));
}Tres detalls que valen la nota: el map retorna la mateixa referència per a les reserves no afectades, de manera que només canvia l'objecte que realment va canviar; new Date().toISOString() s'executa dins del gestor i no durant el render, respectant la puresa (04-03); i filter retorna un array nou per definició, de manera que no cal copiar-lo abans.
Conclusió
useState és molt més del que semblava a 02-04. Ara saps que retorna un parell [valor, actualitzador] en què l'actualitzador és estable per sempre, que el valor és una instantània congelada del render en curs i que per això tres setComptador(comptador + 1) seguits sumen només un. La forma funcional resol aquest cas perquè encua funcions que React aplica en cadena; l'agrupació d'actualitzacions —automàtica a tot arreu des de React 18— converteix diversos canvis en un únic render; i la inicialització mandrosa evita repetir càlculs cars a cada repintat. Tens a més el receptari complet d'actualització immutable per a arrays i objectes aplicat a les reserves de CicloUrbano, cinc principis per estructurar l'estat sense duplicats, contradiccions ni redundàncies, i el truc de la prop key per reiniciar un component sencer quan canvia el que representa.
Amb això pots gestionar qualsevol dada que visqui dins de React. Però una aplicació real no viu sola: hi ha temporitzadors que marquen la disponibilitat de les estacions, esdeveniments del navegador, un títol de pestanya que actualitzar i, sobretot, un servidor del qual portar les bicicletes. Tot això queda fora de React, i connectar-s'hi té les seves pròpies regles: quan començar, quan aturar-se i com no deixar brossa pel camí. És el model de sincronització amb sistemes externs que es va anunciar a 04-03 i que per fi toca desenvolupar. La lliçó següent és Hook useEffect.
Curs de React
Mòdul 1: Introducció a React
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
