El mòdul anterior va acabar amb un diagnòstic incòmode: l'estat de CicloUrbano està repartit per tota l'aplicació —la sessió a ContextUsuari, el tema a ContextTema, els avisos a ContextAvisos, les reserves en un reductor dins de ContextReserves, el filtre del catàleg a la URL i el terme de cerca en un useState de PaginaCataleg— i ningú sabria dir amb seguretat on ha d'anar la propera dada que aparegui. La temptació en aquest punt és triar una biblioteca i confiar que ordeni el desordre. És exactament la decisió equivocada, i en l'ordre equivocat. Primer es classifica l'estat, després es tria l'eina, perquè la meitat dels problemes que la gent intenta resoldre amb Redux desapareixen simplement col·locant cada dada on li correspon. En aquesta lliçó construiràs una taxonomia de l'estat, l'aplicaràs a l'inventari real de CicloUrbano, aprendràs un arbre de decisió que serveixi per a qualsevol dada nova, entendràs els dos errors simètrics —elevar massa i globalitzar massa aviat—, recuperaràs la distinció entre estat i valor derivat, i situaràs en un mapa les opcions disponibles. Al final tindràs una auditoria escrita de CicloUrbano i el pla del que reordena cada lliçó del mòdul.
Contingut
- Què significa realment «gestionar l'estat»
- La taxonomia: sis tipus d'estat
- Recorregut tipus per tipus sobre l'inventari de CicloUrbano
- L'arbre de decisió
- Els dos errors simètrics: elevació excessiva i globalització prematura
- Font única de veritat i estat derivat
- Panorama d'opcions: què existeix i quan ho triaries
- Auditoria de l'estat actual de CicloUrbano
- Pla del mòdul
- Què significa realment «gestionar l'estat»
Fins ara has après mecanismes: useState guarda un valor entre renders, useReducer concentra transicions, useContext evita passar props per deu nivells, useSearchParams posa un valor a la URL. Gestionar l'estat no és aprendre un mecanisme més. És respondre, per cada dada de l'aplicació, a tres preguntes:
- Qui la posseeix? És a dir, quin és l'únic lloc on aquesta dada existeix de veritat. Tota la resta l'ha de llegir d'allà, no copiar-la.
- Qui la necessita? El conjunt de components que la llegeixen o la canvien. Aquest conjunt determina fins on ha de pujar.
- Quan caduca? Una dada local viu el que viu el component. Una dada del servidor pot quedar obsoleta un segon després d'arribar. No són la mateixa mena de cosa.
Quan aquestes tres preguntes es responen malament, apareixen els símptomes coneguts: dues parts de la pantalla que mostren xifres diferents del mateix, un formulari que es buida en navegar, un comptador que es reinicia sense motiu, un canvi de tema que repinta tot el catàleg, un useEffect que sincronitza un estat amb un altre estat. Cap d'aquests símptomes es cura instal·lant un paquet.
La gestió de l'estat és, sobretot, un problema de col·locació. La tria de biblioteca és l'última decisió, no la primera.
- La taxonomia: sis tipus d'estat
Aquesta és la taula central de la lliçó. Val la pena llegir-la a poc a poc, perquè la resta del mòdul s'hi recolza.
| Tipus | Què és | Exemple a CicloUrbano | On ha de viure | Eina | Si el poses on no toca |
|---|---|---|---|---|---|
| Local d'interfície | Un detall visual que només afecta el component que el pinta | Un desplegable obert, la pestanya activa d'un panell, si MenuUsuari està desplegat |
Dins del propi component | useState, useAlternar |
Elevat: el pare repinta mitja pantalla en obrir un menú |
| Compartit entre pocs | Una dada que coordinen dos o tres components germans | La bicicleta seleccionada al catàleg, que ressalten LlistaBicicletes i mostra PanellReserva |
A l'avantpassat comú més proper | Elevar l'estat (04-01) + props | Global: apareix en un magatzem que no li correspon i ningú sap qui el canvia |
| Global d'aplicació | Una dada que llegeix mitja aplicació i canvia poc | Sessió (usuari, esOperari), tema visual, avisos |
A prop de l'arrel, accessible sense props | Context, Redux Toolkit, Zustand | Passat per props: prop drilling de sis nivells amb props que només serveixen per travessar |
| Del servidor | Una còpia local de dades que viuen en una base de dades remota | Bicicletes, estacions, reserves, usuaris | En una memòria cau amb el seu propi cicle de vida | TanStack Query, RTK Query, loader de l'enrutador |
En useState o a Redux: toca escriure a mà càrrega, error, cancel·lació, revalidació i invalidació |
| De la URL | Una dada que ha de poder compartir-se per enllaç o sobreviure a un recarregat | El filtre del catàleg (?tipo=electrica), el bicicletaId de la fitxa |
A la mateixa adreça | useSearchParams, useParams (06-02) |
En useState: l'enllaç que envies a un company no mostra el que tu veus |
| De formulari | Els valors que l'usuari està escrivint, més la seva validació | L'esborrany de FormulariReserva |
Al costat del formulari, o en un reductor propi | useState controlat, useReducer, React Hook Form |
Global: cada tecla despatxa una acció i repinta tot el que llegeixi el magatzem |
Fixa't en un detall que resultarà decisiu a 07-06: l'estat del servidor no és una variant de l'estat global, és una categoria a part. Comparteix amb ell que el llegeix mitja aplicació, però es diferencia en l'essencial: no el posseeixes tu, es queda obsolet sol i algú altre el pot canviar mentre tu el mires.
- Recorregut tipus per tipus sobre l'inventari de CicloUrbano
La taula és abstracta fins que s'aplica. Anem dada per dada amb el que hi ha escrit avui al projecte.
3.1 Local d'interfície: el desplegable de MenuUsuari
// src/components/MenuUsuari.jsx
function MenuUsuari() {
const [obert, alternar] = useAlternar(false); // hook de 05-06
const { usuari, tancarSessio } = useUsuari();
// …
}obert és l'exemple canònic d'estat local: ningú fora de MenuUsuari necessita saber-ho, no ha de sobreviure a una navegació i no té sentit compartir-lo per enllaç. Si l'elevessis a Disseny, obrir el menú provocaria un render de Disseny i, amb ell, de Capcalera, MollesDePa i tot l'<Outlet />. Cost enorme a canvi de res.
Regla: si en escriure l'estat al component ningú protesta, deixa'l allà. L'estat puja quan algú el necessita, no per si de cas.
3.2 Compartit entre pocs: la bicicleta seleccionada
Al catàleg, LlistaBicicletes marca visualment la targeta triada i PanellReserva mostra les seves dades. Són germans, així que la dada viu al seu avantpassat comú, PaginaCataleg, i baixa per props. És exactament el que vas fer a 04-01.
// src/pagines/PaginaCataleg.jsx (fragment)
const [idSeleccionada, setIdSeleccionada] = useState(null);
// …
<LlistaBicicletes bicicletes={visibles} idSeleccionada={idSeleccionada} alSeleccionar={setIdSeleccionada} />
<PanellReserva bicicleta={visibles.find((bici) => bici.id === idSeleccionada)} />Dos nivells de props no són un problema. El prop drilling comença a doldre a partir de tres o quatre nivells de components que reben una prop només per passar-la, sense usar-la.
3.3 Global d'aplicació: sessió, tema i avisos
Els tres compleixen el mateix perfil: els llegeix molta gent, són lluny entre ells a l'arbre i canvien poc.
| Dada | Qui la llegeix | Freqüència de canvi |
|---|---|---|
usuari, esOperari |
Capcalera, MenuUsuari, RutaProtegida, RequereixRol, PaginaTaller, FormulariReserva |
Molt baixa: en entrar i en sortir |
| Tema visual | BotoTema i l'atribut data-tema del document |
Molt baixa: quan l'usuari el prem |
| Avisos | LlistaAvisos els pinta; qualsevol els pot llançar |
Mitjana: a cada operació que informa |
Que la freqüència de canvi sigui baixa és el que fa del context una elecció raonable per als dos primers. Amb els avisos ja comencen a aparèixer matisos, i aquest matís és el tema de 07-02.
3.4 Del servidor: bicicletes, estacions, reserves i usuaris
Avui, a CicloUrbano, viuen a src/dades/domini.js com a constants importades. Això és una simplificació didàctica que ha aguantat sis mòduls, però és mentida: en una aplicació real, bici-002 està llogada perquè algú l'ha llogat fa tres minuts des d'un altre dispositiu, i la teva còpia local no se n'ha assabentat.
// src/dades/domini.js — la drecera que hem usat fins ara
export const bicicletes = [
{ id: 'bici-001', model: 'Urbana Clàssica', tipus: 'urbana', estat: 'disponible', estacioId: 'est-01', preuHora: 2.5 },
{ id: 'bici-002', model: 'Elèctrica Pro', tipus: 'electrica', estat: 'alquilada', estacioId: 'est-01', preuHora: 4.0 },
// …
];A 07-06 això passa a una API fictícia servida amb json-server, i amb ella apareix tota la llista de problemes que justifica una biblioteca dedicada. De moment n'hi ha prou de marcar-ho a l'inventari: aquests quatre conjunts no són estat del client.
3.5 De la URL: el filtre del catàleg
Ja ho vas resoldre a 06-02: ?tipo=electrica viu a l'adreça, es llegeix amb useSearchParams i tipusTriat no existeix com a useState. La prova que la decisió va ser correcta és que pots copiar la barra d'adreces, enganxar-la a un company i veure els dos el mateix. Aquest és el criteri: si la dada ha de poder viatjar en un enllaç, va a la URL, i en cap altre lloc.
Un avís important per a la resta del mòdul: quan arribi Redux, hi haurà una temptació claríssima de moure el filtre al magatzem «per tenir-ho tot junt». No ho facis. Duplicaria la font de veritat i hauries de sincronitzar URL i magatzem amb un efecte, que és justament el que evita useSearchParams.
3.6 De formulari: l'esborrany de la reserva
L'esborrany de FormulariReserva és efímer, canvia a cada pulsació i només interessa mentre el formulari està obert. Viu a reductorReserves perquè comparteix transicions amb l'enviament (enviament_iniciat, reserva_creada, enviament_fallit), no perquè sigui global. És un cas de llibre de per què la classificació importa: l'esborrany està en un context, però no és estat global.
- L'arbre de decisió
Cada vegada que aparegui una dada nova, recorre'l de dalt a baix i para't a la primera resposta afirmativa.
flowchart TD
A["Dada nova"] --> B{"Ve d'un servidor?"}
B -- Sí --> C["Memòria cau d'estat de servidor<br/>TanStack Query · 07-06"]
B -- No --> D{"Ha de poder compartir-se<br/>per enllaç o sobreviure<br/>a un recarregat?"}
D -- Sí --> E["URL<br/>useSearchParams / useParams"]
D -- No --> F{"L'usa un sol<br/>component?"}
F -- Sí --> G["useState local"]
F -- No --> H{"Uns pocs germans<br/>propers?"}
H -- Sí --> I["Elevar a l'avantpassat comú<br/>+ props"]
H -- No --> J{"El llegeix mitja aplicació<br/>i canvia poc?"}
J -- Sí --> K["Context · 07-02"]
J -- No --> L{"Canvia sovint,<br/>hi ha molts consumidors<br/>o la lògica és complexa?"}
L -- Sí --> M["Magatzem dedicat<br/>Redux Toolkit · 07-03…07-05"]
L -- No --> K
Dues observacions sobre l'arbre:
- La primera pregunta és la del servidor, i és deliberat. És la que més gent es salta i la que més feina estalvia encertar. Ficar dades remotes en un magatzem de client és l'error d'arquitectura més car d'aquest mòdul.
- La branca de Redux és l'última. No perquè Redux sigui dolent, sinó perquè arribar-hi vol dir haver descartat abans cinc alternatives més barates. Quan arribes a Redux havent descartat aquestes cinc, Redux és una decisió excel·lent.
- Els dos errors simètrics: elevació excessiva i globalització prematura
Els principiants posen l'estat massa avall i pateixen; els intermedis sobrecorregeixen i el posen massa amunt. Tots dos extrems tenen símptomes reconeixibles.
5.1 Elevació excessiva
Consisteix a pujar un estat més amunt d'on cal «per si algun dia el necessita algú més».
Símptomes concrets:
- Obrir un desplegable repinta la capçalera, les molles de pa i la pàgina sencera.
- Components que reben cinc props de les quals n'usen una: les altres quatre només hi passen de llarg.
- Un pare amb nou
useStateque no tenen res a veure entre si. - Canviar un component fulla obliga a tocar tres fitxers per sobre.
// ANTIPATRÓ: l'estat d'un detall visual, elevat sense necessitat
function Disseny() {
const [menuObert, setMenuObert] = useState(false); // ← només l'usa MenuUsuari
return (
<div>
<Capcalera menuObert={menuObert} alAlternarMenu={setMenuObert} />
<Outlet />
</div>
);
}Capcalera no usa menuObert: només el transporta fins a MenuUsuari. I cada pulsació del menú provoca un render de Disseny, és a dir, de tota l'aplicació.
5.2 Globalització prematura
Consisteix a instal·lar una biblioteca d'estat global abans de tenir un problema que la justifiqui.
Símptomes concrets:
- Un magatzem amb
menuObert,modalVisibleipestanyaActiva: estat local disfressat de global. - Accions que només despatxa un component i que només llegeix aquest mateix component.
- Cinc fitxers tocats (acció, reductor, selector, component, prova) per afegir una casella de verificació.
- Ningú pot eliminar un camp del magatzem perquè ningú sap qui el llegeix.
| Elevació excessiva | Globalització prematura | |
|---|---|---|
| Què es fa malament | L'estat puja més del necessari | Tot acaba en un magatzem únic |
| Cost principal | Renders innecessaris i props de pas | Complexitat, indirecció i cerimònia |
| Com es detecta | Props que no s'usen, renders en cascada | Estat local dins del magatzem |
| Com es corregeix | Baixar l'estat al component que l'usa | Treure del magatzem el que no sigui global |
La correcció de tots dos és la mateixa frase: l'estat ha de viure en el punt més baix possible que continuï sent comú a tots els seus consumidors. Ni més amunt, ni més avall.
- Font única de veritat i estat derivat
Recuperem aquí, amb més conseqüències, una cosa que ja vas veure a 05-01: no guardis com a estat res que puguis calcular a partir d'un altre estat.
// ANTIPATRÓ: estat duplicat i sincronitzat amb un efecte
const [bicicletes, setBicicletes] = useState(dadesInicials);
const [tipus, setTipus] = useState('todos');
const [visibles, setVisibles] = useState(dadesInicials); // ← derivat guardat com a estat
useEffect(() => {
setVisibles(bicicletes.filter((bici) => tipus === 'todos' || bici.tipus === tipus));
}, [bicicletes, tipus]);Tres problemes encadenats: hi ha un render de més (React pinta amb visibles desactualitzat i torna a pintar després de l'efecte), existeix una finestra en què visibles i tipus es contradiuen, i qualsevol que en el futur canviï bicicletes sense passar per l'efecte trenca la coherència.
// CORRECTE: el derivat es calcula durant el render
const [bicicletes, setBicicletes] = useState(dadesInicials);
const tipus = parametres.get('tipo') ?? 'todos'; // la URL és la font de veritat
const visibles = bicicletes.filter((bici) => tipus === 'todos' || bici.tipus === tipus);visibles deixa d'existir com a estat. No es pot desincronitzar, perquè es recalcula a cada render a partir de les dues úniques fonts de veritat. Si algun dia aquest càlcul resulta car, la resposta és memoïtzar-lo amb useMemo (08-03), no convertir-lo en estat.
Casos d'estat derivat que apareixen a CicloUrbano i que no s'han de guardar:
| Valor | Es calcula a partir de | Mai el guardis perquè |
|---|---|---|
esOperari |
usuari.rol === 'operario' |
Es desincronitza en canviar de sessió |
visibles |
bicicletes + filtre de la URL + terme de cerca |
Es desincronitza a cada canvi de filtre |
potEnviar |
Errors de validació + estatEnviament |
Es desincronitza en escriure al formulari |
totalReserva |
preuHora × hores |
Es desincronitza en canviar les hores |
placesLliures |
estacio.places − bicicletes a l'estació |
Es desincronitza en moure una bicicleta |
Regla: si pots escriure el valor com una expressió d'altres valors, és un derivat. Escriu-lo com una expressió.
- Panorama d'opcions: què existeix i quan ho triaries
Aquesta taula és el mapa del terreny. No és una recomanació única: la columna que importa és «quan ho triaries».
| Opció | Què resol | Quan ho triaries | Cost d'entrada |
|---|---|---|---|
| Props + elevar l'estat | Coordinar uns pocs components propers | Sempre que la distància sigui d'un a tres nivells | Nul: ja és a React |
| Context | Evitar props de pas per a dades ambientals | Sessió, tema, idioma, avisos: pocs canvis, molts lectors | Baix, però amb cost de rendiment si el valor canvia sovint (07-02) |
useReducer + context |
Lògica de transició complexa compartida | Un domini amb moltes accions i un arbre ampli de consumidors | Baix: no hi ha dependències noves |
| Redux Toolkit | Magatzem únic, accions traçables, eines de depuració | Aplicacions grans, equips de diverses persones, lògica d'estat que cal auditar | Mitjà: una dependència, un vocabulari i una estructura de carpetes |
| Zustand | Magatzem global mínim amb selecció granular, sense proveïdor | Vols estat global sense la cerimònia de Redux i no necessites viatge en el temps | Molt baix: un fitxer i un hook |
| Jotai | Estat atòmic: unitats petites que es combinen i només repinten a qui les llegeix | Moltes peces independents d'estat amb dependències entre elles | Baix, però exigeix pensar en àtoms, no en objectes |
| TanStack Query | Estat del servidor: memòria cau, càrrega, error, revalidació, invalidació | Tan bon punt l'aplicació llegeixi dades d'una API. Gairebé sempre | Mitjà, i s'amortitza a la primera pantalla |
Sobre Zustand i Jotai, el just per situar-los i sense tutorial, perquè aquest mòdul ensenya Redux Toolkit:
- Zustand defineix un magatzem com una funció que retorna estat i accions, i els components s'hi subscriuen amb un selector:
const usuari = useMagatzem((s) => s.usuari). No hi ha proveïdor, no hi ha accions ambtypei només es repinta qui llegeix el que ha canviat. És l'alternativa habitual quan Redux sembla massa cerimònia. - Jotai inverteix el model: en lloc d'un objecte gran, defineix àtoms petits (
const atomTema = atom('clar')) que es llegeixen ambuseAtom. Els àtoms derivats es recalculen sols i només repinta qui depèn de l'àtom que ha canviat.
Totes dues són opcions legítimes i cap substitueix TanStack Query per a les dades remotes. La tria més habitual avui en una aplicació mitjana no és «Redux o context», sinó «una eina per a l'estat del client i una altra per al del servidor».
- Auditoria de l'estat actual de CicloUrbano
Amb la taxonomia a la mà, aquesta és la foto de l'aplicació al final del mòdul 6 i el veredicte de cada peça.
| Dada | On és avui | Tipus real | Veredicte | Lliçó que la tracta |
|---|---|---|---|---|
usuari, esOperari, carregantSessio |
ContextUsuari |
Global | Correcte; el valor s'ha d'estabilitzar | 07-02, després sliceSessio a 07-04 |
| Tema visual | ContextTema |
Global | Correcte i es queda així tot el mòdul | 07-02 |
avisos, mostrarAvis |
ContextAvisos |
Global | Correcte, però cal separar estat d'accions | 07-02 |
reserves, esborrany, estatEnviament, error |
ContextReserves amb useReducer |
Barreja: la llista és del servidor, l'esborrany és de formulari | A dividir | 07-04 i 07-06 |
Filtre ?tipo= |
URL | De URL | Correcte; no es mou a Redux | Es manté |
terme de cerca |
useState a PaginaCataleg |
Local avui, compartit tan bon punt el llegeixi una altra pantalla | Revisable | 07-04 (sliceCataleg) |
bicicletes, estacions |
Constants a src/dades/domini.js |
Del servidor | A moure a una memòria cau de consultes | 07-06 |
idSeleccionada |
useState a PaginaCataleg |
Compartit entre pocs | Correcte: es queda on és | Es manté |
obert de MenuUsuari, modals |
useState / useAlternar locals |
Local d'interfície | Correcte: mai entra al magatzem | Es manté |
Dues conclusions de l'auditoria que convé subratllar abans de continuar:
- La major part del que hi ha està ben col·locat. El mòdul no tirarà per terra la feina dels mòduls 5 i 6: reordenarà tres coses i n'extraurà una quarta.
- El problema més greu no és el context, és que les dades de domini es tracten com a estat del client. És el que arregla 07-06, i és el canvi amb més impacte de tot el mòdul.
- Pla del mòdul
flowchart LR
A["07-01<br/>Classificar"] --> B["07-02<br/>Context ben fet"]
B --> C["07-03<br/>Magatzem Redux"]
C --> D["07-04<br/>Slices i selectors"]
D --> E["07-05<br/>Connectar a React"]
E --> F["07-06<br/>Estat del servidor"]
| Lliçó | Què reordena de CicloUrbano |
|---|---|
| 07-02 API de Context | Divideix ContextReserves en estat i accions, compon els quatre proveïdors en un sol Proveidors i estableix on el context deixa de rendir |
| 07-03 Redux: Introducció | Crea src/magatzem/magatzem.js amb configureStore i el proveeix a main.jsx, amb DevTools funcionant |
| 07-04 Accions i Reductors | Escriu sliceReserves, sliceCataleg i sliceSessio amb els seus selectors i la seva càrrega asíncrona |
| 07-05 Connectant a React | Reescriu PaginaCataleg, PaginaReserves, PanellReserves i MenuUsuari amb useSelector i useDispatch, i decideix què es queda fora |
| 07-06 Estat del Servidor | Aixeca l'API amb json-server, substitueix useFetchBicicletes per useBicicletes sobre TanStack Query i fixa l'arquitectura final |
Errors Comuns i Consells
Error 1: triar la biblioteca abans de classificar l'estat. «Farem servir Redux» no és una decisió d'arquitectura, és un ajornament. Classifica primer; sovint descobriràs que la meitat del teu «estat global» era estat del servidor i l'altra meitat, estat local.
Error 2: tractar les dades del servidor com a estat normal. És l'error més car i el més freqüent. Si la dada té un id que existeix en una base de dades, no és teva: és una còpia que caduca.
Error 3: guardar derivats. Cada useEffect la missió única del qual és fer setAlgo(...) a partir d'un altre estat és un derivat mal guardat. Esborra'l i calcula l'expressió durant el render.
Error 4: moure a un magatzem global el que ja viu a la URL. Acabes amb dues fonts de veritat i un efecte que les sincronitza. La URL guanya sempre per al que ha de ser compartible per enllaç.
Error 5: confondre «l'usen molts components» amb «canvia molt». Són eixos diferents i determinen coses diferents: el primer decideix on viu, el segon decideix amb quina eina. Una dada llegida per vint components que canvia una vegada per sessió és el cas ideal del context; una llegida per vint components que canvia a cada pulsació és el pitjor cas.
Consell 1: escriu l'inventari. Una taula de tres columnes —dada, tipus, on viu— al README del projecte evita mesos de discussions. Quan algú afegeixi una dada nova, la taula li dirà on posar-la.
Consell 2: comença sempre pel useState local. Pujar un estat després és un refactor de deu minuts. Baixar-lo des d'un magatzem global al qual ja s'hi han enganxat vuit components és un refactor d'una tarda.
Consell 3: mesura abans d'optimitzar la col·locació. Si sospites que un context està provocant renders de més, comprova-ho amb el Profiler (08-05) en lloc de reestructurar a cegues.
Exercicis
Exercici 1. Classifica aquestes sis dades noves de CicloUrbano segons la taxonomia de l'apartat 2 i indica on han de viure i amb quina eina:
- El text que l'operari escriu al cercador de la
PaginaTaller. - La llista d'incidències d'una estació, que arriba de
GET /estaciones/est-02/incidencias. - La unitat de preu triada (€/hora o €/dia), que afecta totes les targetes i la fitxa.
- Si la targeta
TargetaBicicletamostra la seva descripció llarga desplegada. - L'ordre del catàleg (per preu o per model), que ha de poder enviar-se per enllaç.
- El nombre de reserves actives que mostra el distintiu de la
Capcalera.
Exercici 2. El component següent té tres problemes de gestió de l'estat. Identifica'ls i reescriu-lo.
function PaginaEstacions() {
const [estacions, setEstacions] = useState([]);
const [barri, setBarri] = useState('todos');
const [visibles, setVisibles] = useState([]);
const [totalPlaces, setTotalPlaces] = useState(0);
const [detallObert, setDetallObert] = useState(null);
useEffect(() => {
setVisibles(estacions.filter((est) => barri === 'todos' || est.barri === barri));
}, [estacions, barri]);
useEffect(() => {
setTotalPlaces(visibles.reduce((suma, est) => suma + est.places, 0));
}, [visibles]);
// …
}Exercici 3. Un company proposa: «Posem a Redux tota l'aplicació: les bicicletes, el filtre del catàleg, el tema, el menú desplegable de MenuUsuari i l'esborrany del formulari de reserva. Així ho tenim tot en un lloc i sempre sabem on mirar.» Escriu una resposta raonada, dada per dada, indicant quines sí i quines no, i per què.
Solucions
Solució 1.
| Dada | Tipus | On viu | Eina | Raonament |
|---|---|---|---|---|
| 1. Cercador del taller | Local d'interfície (o de formulari) | A PaginaTaller |
useState + useDebounce |
Només l'usa aquesta pantalla i no té sentit compartir-lo per enllaç. Si l'equip decideix que sí que ha de ser enllaçable, passa a la URL |
| 2. Incidències d'una estació | Del servidor | Memòria cau de consultes | useQuery(['estaciones', estacionId, 'incidencias']) |
Ve d'una API: la primera pregunta de l'arbre es respon afirmativament i aquí acaba el recorregut |
| 3. Unitat de preu | Global d'aplicació | A prop de l'arrel | Context (o sliceCataleg si ja uses Redux) |
Molts lectors, canvis molt poc freqüents: perfil idèntic al del tema |
| 4. Descripció desplegada | Local d'interfície | Dins de TargetaBicicleta |
useAlternar |
Un detall visual per targeta. Elevar-lo obligaria a guardar un mapa d'identificadors oberts per res |
| 5. Ordre del catàleg | De la URL | A l'adreça | useSearchParams, junt a ?tipo= |
L'enunciat ho diu: ha de poder enviar-se per enllaç |
| 6. Nombre de reserves actives | Derivat de l'estat del servidor | No es guarda | reserves.filter((r) => r.estat === 'activa').length |
No és estat: és un recompte sobre la llista de reserves. Guardar-lo garanteix que algun dia mostri una xifra diferent de la de la llista |
El cas 6 és el més instructiu: la resposta correcta no és «on ho guardo» sinó «no ho guardis».
Solució 2. Els tres problemes:
visiblesés un derivat guardat com a estat, sincronitzat amb un efecte. Sobra l'estat i sobra l'efecte.totalPlacesés un derivat d'un derivat, amb un segon efecte encadenat. Cada canvi debarriprovoca tres renders: un amb dades velles, un altre després del primer efecte i un altre després del segon.estacionsés estat del servidor guardat enuseState, sense càrrega, sense error i sense cancel·lació. És el candidat de 07-06.
function PaginaEstacions() {
// 3) Estat del servidor: a 07-06 això passa a useQuery
const { data: estacions = [], isPending, isError } = useEstacions();
// Estat real: només dues peces
const [barri, setBarri] = useState('todos');
const [detallObert, setDetallObert] = useState(null);
// 1) i 2) Derivats: expressions, no estat
const visibles = estacions.filter((est) => barri === 'todos' || est.barri === barri);
const totalPlaces = visibles.reduce((suma, est) => suma + est.places, 0);
if (isPending) return <IndicadorDeCarrega missatge="Carregant estacions…" />;
if (isError) return <Avis to="error" text="No s'han pogut carregar les estacions." />;
// …
}De cinc variables d'estat es passa a dues. Els dos useEffect desapareixen, i amb ells els renders intermedis i les finestres d'incoherència. detallObert es queda: és estat local d'interfície legítim.
Solució 3. Resposta raonada, dada per dada:
- Bicicletes: no. Són estat del servidor. A Redux hauries d'escriure a mà la càrrega, l'error, la cancel·lació, la revalidació en tornar a la pestanya i la invalidació després de cada escriptura. Van a una memòria cau de consultes (07-06). Si l'equip prefereix no afegir una altra dependència, l'alternativa raonable és RTK Query, que és la mateixa idea dins de Redux, no un
slicea mà. - Filtre del catàleg: no. Ja viu a la URL, que és la font de veritat correcta perquè ha de poder compartir-se per enllaç i sobreviure a un recarregat. Moure'l al magatzem crearia una segona còpia i un efecte de sincronització, amb la incoherència garantida.
- Tema: no cal. Canvia una vegada per sessió i el llegeix un
BotoTemamés l'atributdata-temadel document. El context ho resol sense dependències. Ficar-lo a Redux no està malament, però no aporta res: no hi ha lògica a auditar ni accions a traçar. - Menú desplegable de
MenuUsuari: rotundament no. És estat local d'interfície. Al magatzem el convertiries en global, cada obertura despatxaria una acció que embrutaria l'historial de DevTools i obligaria a comprovar qui més llegeix aquest camp abans de tocar-lo. - Esborrany del formulari: no. Canvia a cada pulsació. Al magatzem, cada tecla seria una acció i un cicle de comparació per a tots els subscriptors. Es queda al costat del formulari, en
useStateo en el seu reductor. - El que sí que aniria a Redux: la sessió (
sliceSessio), els criteris persistents del catàleg que no van a la URL —com el terme de cerca si acaba compartint-se entre pantalles— i la lògica de reserves del client, que és la que té transicions que val la pena auditar.
I l'argument de fons contra la proposta: «tenir-ho tot en un lloc» no és un avantatge si aquest lloc deixa de dir-te res. Un magatzem amb dos-cents camps, dels quals cent cinquanta són locals, és tan difícil de raonar com no tenir magatzem; a més fa que l'eina més valuosa de Redux —l'historial d'accions— quedi sepultada sota el soroll d'obertures de menú.
Conclusió
Abans d'instal·lar res, has posat ordre. Saps que gestionar l'estat és respondre a tres preguntes per cada dada —qui la posseeix, qui la necessita i quan caduca— i que la resposta s'organitza en sis tipus: local d'interfície, compartit entre pocs, global d'aplicació, del servidor, de la URL i de formulari, cadascun amb el seu lloc natural i la seva eina. Tens un arbre de decisió que comença per la pregunta que més estalvia —«ve d'un servidor?»— i que deixa el magatzem dedicat com a última branca, la que s'assoleix després de descartar cinc alternatives més barates. Coneixes els dos errors simètrics i els seus símptomes: l'elevació excessiva, que converteix cada pulsació en un render de tota l'aplicació i omple els components de props de pas, i la globalització prematura, que fica menuObert en un magatzem i obliga a tocar cinc fitxers per afegir una casella. I has recuperat amb més pes la regla de 05-01: una única font de veritat i tota la resta calculat, perquè visibles, esOperari, potEnviar, totalReserva i placesLliures no són estat i guardar-los només garanteix que algun dia es contradiguin.
Sobre aquesta bastida has situat les opcions reals —props i elevar, context, useReducer + context, Redux Toolkit, Zustand, Jotai i TanStack Query— amb el seu cost d'entrada i el seu moment adequat, i has fet l'auditoria de CicloUrbano: la major part està ben col·locada, el filtre continuarà a la URL i els desplegables continuaran sent locals, però ContextReserves barreja estat de formulari amb dades que en realitat són del servidor, i src/dades/domini.js fingeix ser una base de dades.
La primera peça a reordenar és la que ja tens a les mans. El context no és només un mecanisme per evitar props: és una estratègia de gestió d'estat, amb un patró complet de mòdul per domini, un cost de rendiment molt concret que fins ara només s'havia esmentat de passada, i un límit clar més enllà del qual convé una altra eina. La propera lliçó és API de Context.
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
