La lliçó anterior va acabar amb una llista precisa del que el context no resol: eines de depuració, un punt únic on interceptar canvis, viatge en el temps, selecció granular i una convenció compartida per a la lògica asíncrona. Redux existeix exactament per aquesta llista. I convé dir-ho des de la primera línia per desfer el malentès més habitual: Redux no és una manera més ràpida de compartir estat, és una manera més disciplinada de canviar-lo. El que hi compres no és velocitat, és predictibilitat i capacitat de diagnòstic. En aquesta lliçó entendràs quin problema resol Redux, els seus tres principis i per què fan l'aplicació depurable, la seva relació directa amb el useReducer que ja vas escriure a 05-05, per què Redux Toolkit és avui la manera oficial d'usar Redux i què canvia respecte del Redux clàssic que encara veuràs en projectes antics. Després muntaràs el magatzem de CicloUrbano amb configureStore, el proveiràs a main.jsx sense trencar l'ordre de proveïdors ni l'enrutador, i instal·laràs Redux DevTools per veure l'historial d'accions. En acabar, el magatzem existirà i funcionarà, però amb prou feines es consumirà: modelar-lo és cosa de 07-04 i connectar-lo als components, de 07-05.
Contingut
- Què és Redux i quin problema resol
- Els tres principis
- Per què aquests principis fan l'aplicació predictible i depurable
- Relació amb
useReducer: mateix model, escala diferent - El flux unidireccional
- Redux Toolkit: la manera oficial d'usar Redux avui
- Com era el Redux clàssic (codi heretat que cal saber llegir)
- Instal·lació
- El magatzem de CicloUrbano:
src/magatzem/magatzem.js - El middleware per defecte i les seves comprovacions de desenvolupament
- Proveir el magatzem a
main.jsx - Redux DevTools: l'argument més fort
- Quan NO usar Redux
- Què és Redux i quin problema resol
Redux és un contenidor d'estat predictible: un objecte JavaScript que guarda tot l'estat del client de l'aplicació i que només permet canviar-lo enviant accions, objectes que descriuen què ha passat. No existeix cap altra via de modificació.
El problema que resol no és «compartir dades entre components llunyans» —això ho fa el context, i més barat—. El problema és aquest altre, que apareix quan una aplicació creix:
Quan alguna cosa va malament a la pantalla, no saps què va canviar l'estat, quan el va canviar ni qui ho va provocar.
En una aplicació amb quinze components que criden setAlgo des de gestors, efectes i callbacks de peticions, reconstruir la seqüència de successos que va portar a la fallada és arqueologia. Redux ataca això convertint cada canvi en un registre explícit i ordenat: una llista d'accions, amb la seva càrrega útil i la seva marca temporal, que pots recórrer cap enrere.
La segona cosa que resol, més prosaica però igual de real: dona una convenció. En un equip de sis persones, «on va aquesta dada i com es canvia» deixa de ser una discussió per a cada funcionalitat.
- Els tres principis
Redux es recolza en tres regles. No són recomanacions: són les que fan possible tota la resta.
Principi 1: font única de veritat
Tot l'estat de l'aplicació viu en un únic objecte, dins d'un únic magatzem.
// Forma aproximada de l'estat de CicloUrbano (es defineix a 07-04)
{
reserves: { entitats: {…}, ids: […], estatCarrega: 'inactiu', error: null },
cataleg: { tipus: 'todos', terme: '', ordre: 'model' },
sessio: { usuari: null, carregant: true }
}Conseqüències pràctiques: l'estat complet es pot serialitzar i adjuntar a un informe de fallo; es pot persistir a localStorage i restaurar; es poden escriure proves partint d'un estat inicial concret; i no hi ha dos llocs on la mateixa dada pugui dir coses diferents.
Principi 2: l'estat és de només lectura
L'única manera de canviar l'estat és despatxar una acció, un objecte pla que descriu què ha passat.
Ningú escriu estat.reserves[0].estat = 'confirmada'. La conseqüència és que existeix un coll d'ampolla deliberat: tots els canvis passen pel mateix lloc, així que aquest lloc els pot registrar, mesurar, interceptar o repetir.
Principi 3: els canvis es fan amb funcions pures
Un reductor és una funció pura (estat, accio) => nouEstat. No modifica els seus arguments, no fa peticions, no llegeix l'hora ni genera identificadors aleatoris, i amb les mateixes entrades retorna sempre la mateixa sortida.
És literalment la definició que vas aprendre a 05-05 per a reductorReserves.
- Per què aquests principis fan l'aplicació predictible i depurable
Els tres principis junts produeixen una propietat molt concreta:
Estat inicial + llista d'accions = estat actual. Sempre, sense excepcions.
D'aquí surt tota la resta:
| Propietat | Com l'habiliten els principis |
|---|---|
| Reproduir un fallo | Si guardes l'estat inicial i les accions, pots reproduir exactament la sessió de l'usuari que va fallar |
| Viatge en el temps | Com que els reductors són purs, tornar a aplicar les primeres N accions dona l'estat en el moment N |
| Registre automàtic | El coll d'ampolla del principi 2 permet registrar cada canvi sense tocar els components |
| Proves trivials | Provar un reductor és cridar una funció amb dos arguments i comparar la sortida (Mòdul 9) |
| Desfer / refer | Amb estats immutables, guardar els anteriors és guardar referències |
| Depuració per diferències | DevTools mostra l'«abans i després» de cada acció, així que el canvi inesperat es localitza en segons |
I el preu, que cal dir igual de clar: més cerimònia. Canviar una dada deixa de ser una línia i passa a ser una acció, un cas en un reductor i un selector. Aquesta cerimònia és la que es paga a canvi de la taula de dalt. Si la teva aplicació no necessita res d'aquesta taula, no la paguis.
- Relació amb
useReducer: mateix model, escala diferent
useReducer: mateix model, escala diferentAixò ja es va anunciar a 05-05: el model de Redux és exactament el de useReducer. Compara.
// El que vas escriure a 05-05
const [estat, despatxar] = useReducer(reductorReserves, ESTAT_INICIAL_RESERVES);
despatxar({ tipus: 'reserva_confirmada', idReserva: 'res-01' });
// El mateix a Redux
const estat = magatzem.getState();
magatzem.dispatch({ type: 'reserves/reservaConfirmada', payload: 'res-01' });Un reductor de Redux i el teu reductorReserves són la mateixa mena de funció. Les diferències són d'abast, no de concepte:
useReducer |
Redux | |
|---|---|---|
| On viu l'estat | Dins de l'arbre de React, en un component | En un objecte independent, fora de React |
| Abast | Un subarbre | Tota l'aplicació |
| Accés | useContext des dels descendents del proveïdor |
useSelector des de qualsevol component |
| Nom del camp de tipus | Lliure; al curs usem tipus |
Obligatòriament type |
| Càrrega útil | Camps lliures a l'acció | Convenció payload |
| Interceptar canvis | No | Middleware |
| Eines | Cap | DevTools |
| Combinar dominis | Un reductor per proveïdor | Diversos reductors en un magatzem |
Presta atenció a dues files: a Redux el camp s'anomena type (en anglès i sense negociació, perquè ho exigeix la biblioteca i les seves eines), i la càrrega útil va per convenció a payload. A la resta del curs hem usat tipus en català perquè era codi nostre; a partir d'aquí, quan l'objecte sigui una acció de Redux, s'usen type i payload. És la mateixa regla de sempre: les APIs i les paraules clau no es tradueixen.
Que l'estat visqui fora de React té conseqüències que ja es van apuntar a 07-02: el magatzem es pot llegir des d'una utilitat, des d'un interceptor de peticions o des d'una prova sense muntar cap component, i sobreviu al desmuntatge de qualsevol part de l'arbre.
- El flux unidireccional
flowchart LR
A["Vista<br/>PaginaReserves"] -- "1. dispatch(accio)" --> B["Magatzem"]
B -- "2. (estat, accio)" --> C["Reductor<br/>funció pura"]
C -- "3. estat nou" --> B
B -- "4. notifica" --> D["Subscriptors<br/>useSelector"]
D -- "5. repinta si la seva porció ha canviat" --> A
E["Middleware<br/>registre · asincronia · DevTools"] -.- B
Els cinc passos, amb noms concrets de CicloUrbano:
- L'usuari prem «Confirmar» a
PanellReservesi el gestor despatxa{ type: 'reserves/reservaConfirmada', payload: 'res-01' }. - El magatzem passa l'acció pel middleware —que la pot registrar, retardar o transformar— i després crida el reductor arrel amb l'estat actual i l'acció.
- El reductor retorna un estat nou; mai modifica el que va rebre.
- El magatzem guarda l'estat nou i notifica els seus subscriptors.
- Cada component subscrit comprova si la seva porció ha canviat i es repinta només si és així.
«Unidireccional» significa que les fletxes mai van al revés: una vista no modifica l'estat directament, ni un reductor provoca una navegació, ni el magatzem crida un component. Aquesta disciplina és el que fa que el pas 5 sigui l'únic lloc on mirar quan la pantalla no mostra el que s'esperava.
Fixa't en el pas 5 i en la paraula «la seva porció»: aquí hi ha la selecció granular que el context no podia donar. El mecanisme exacte és useSelector, i és el tema de 07-05.
- Redux Toolkit: la manera oficial d'usar Redux avui
Redux té més de deu anys i arrossega una reputació de verbositat molt merescuda... referida a com s'escrivia el 2016. Des del 2019, Redux Toolkit (RTK) és la manera oficial i recomanada d'usar Redux, i així ho diu la mateixa documentació del projecte. Aquest curs ensenya Redux exclusivament amb RTK.
Què inclou el paquet:
| Utilitat | Per a què serveix | On es veu |
|---|---|---|
configureStore |
Crea el magatzem amb bons valors per defecte i DevTools ja connectades | Aquesta lliçó |
createSlice |
Genera reductor i creadors d'acció a partir d'una descripció | 07-04 |
createAsyncThunk |
Lògica asíncrona amb els tres estats (pending/fulfilled/rejected) |
07-04 |
createSelector |
Selectors derivats memoïtzats (ve de Reselect) | 07-04 i 07-05 |
createEntityAdapter |
Estat normalitzat per id, amb operacions ja escrites | 07-04 |
| Immer, inclòs | Permet escriure codi «que muta» sense mutar de veritat | 07-04 |
| RTK Query, inclòs | Memòria cau de dades de servidor, alternativa a TanStack Query | 07-06 |
El que canvia respecte del Redux clàssic:
| Redux clàssic (2016) | Amb Redux Toolkit | |
|---|---|---|
| Boilerplate | Constants de tipus, creadors d'acció, switch i combineReducers, cada cosa al seu fitxer |
Un createSlice genera accions i reductor alhora |
| Immutabilitat | A mà, amb ... imbricats; un oblit és un fallo silenciós |
Immer: escrius estat.reserves.push(x) i continua sent immutable |
| Asincronia | redux-thunk instal·lat i configurat a part, o redux-saga |
createAsyncThunk, inclòs i amb patró definit |
| Configuració de DevTools | window.__REDUX_DEVTOOLS_EXTENSION__ a mà a createStore |
Connectades per defecte en desenvolupament |
| Comprovacions | Cap: mutar l'estat per error no avisa | Avisos en desenvolupament si mutes o guardes alguna cosa no serialitzable |
| Mida | redux és minúscul, però s'acaben sumant 3-4 paquets |
RTK pesa més, i a canvi els substitueix tots |
| Fitxers per funcionalitat | 3 o 4 (tipus.js, accions.js, reductor.js, selectors.js) |
1 (sliceX.js) |
L'única fila que juga en contra d'RTK és la mida: RTK més react-redux ronden els 15-20 kB comprimits davant els ~2 kB de redux a seques. A canvi t'estalvies Reselect, redux-thunk, la configuració de DevTools i una quantitat considerable de codi propi. En qualsevol aplicació real, el canvi compensa.
- Com era el Redux clàssic (codi heretat que cal saber llegir)
⚠️ Aquest apartat és exclusivament per saber llegir projectes antics. El codi que veuràs aquí no s'ha d'escriure mai en codi nou. S'inclou perquè te'l trobaràs en aplicacions que porten anys en producció i perquè explica d'on vénen els conceptes que RTK automatitza. Després d'aquest apartat no tornarà a aparèixer al curs.
// ⚠️ CODI HERETAT — NO ESCRIURE AIXÍ AVUI
// tipus.js
export const RESERVA_CONFIRMADA = 'RESERVA_CONFIRMADA';
// accions.js
export function confirmarReserva(idReserva) {
return { type: RESERVA_CONFIRMADA, payload: idReserva };
}
// reductor.js
import { RESERVA_CONFIRMADA } from './tipus.js';
const estatInicial = { reserves: [] };
function reductorReserves(estat = estatInicial, accio) {
switch (accio.type) {
case RESERVA_CONFIRMADA:
return {
...estat,
reserves: estat.reserves.map((reserva) =>
reserva.id === accio.payload ? { ...reserva, estat: 'confirmada' } : reserva
)
};
default:
return estat;
}
}
// magatzem.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const reductorArrel = combineReducers({ reserves: reductorReserves, cataleg: reductorCataleg });
const magatzem = createStore(
reductorArrel,
applyMiddleware(thunk)
);Què mirar en aquest fragment quan te'l trobis:
- Les constants de tipus existien per evitar errates en cadenes de text repartides per diversos fitxers. RTK les elimina:
createSlicegenera el tipus a partir del nom. - El
switchambdefault: return estatés obligatori en Redux clàssic, perquè cada acció passa per tots els reductors. Compte amb la diferència respecte delreductorReservesde 05-05, on eldefaultllançava un error: allà era correcte perquè només hi arribaven les accions d'aquell reductor. - Els
...imbricats són la immutabilitat a mà. Amb dos nivells ja són difícils de llegir i amb tres són un focus de fallos. combineReducersexplícit iapplyMiddleware(thunk): tots dos desapareixen ambconfigureStore, que fa el mateix per dins.- És habitual trobar-ho junt a
connect,mapStateToPropsimapDispatchToProps, l'API prèvia als hooks. S'explica a 07-05, i també només per llegir.
Tot el que fa aquest codi ho fa RTK amb una fracció del text, sense possibilitat d'oblidar un default ni de mutar per accident.
- Instal·lació
Dos paquets i per a què serveix cadascun:
| Paquet | Què aporta |
|---|---|
@reduxjs/toolkit |
Redux en si mateix, més configureStore, createSlice, createAsyncThunk, createSelector, createEntityAdapter, Immer i RTK Query |
react-redux |
L'enganxina amb React: <Provider>, useSelector, useDispatch |
No instal·lis redux, redux-thunk ni reselect per separat. Vénen inclosos a RTK, i afegir-los a part pot acabar en dues còpies de Redux al mateix paquet final.
- El magatzem de CicloUrbano:
src/magatzem/magatzem.js
src/magatzem/magatzem.jsconfigureStore és el punt d'entrada. Rep un objecte de configuració la clau obligatòria del qual és reducer.
// src/magatzem/magatzem.js
import { configureStore } from '@reduxjs/toolkit';
import reductorReserves from '../funcionalitats/reserves/sliceReserves.js';
import reductorCataleg from '../funcionalitats/cataleg/sliceCataleg.js';
import reductorSessio from '../funcionalitats/sessio/sliceSessio.js';
export const magatzem = configureStore({
// El mapa de reductors defineix la FORMA de l'estat global:
// estat.reserves · estat.cataleg · estat.sessio
reducer: {
reserves: reductorReserves,
cataleg: reductorCataleg,
sessio: reductorSessio
}
});Tres coses que fa configureStore sense que li ho hagis de demanar:
- Combina els reductors. L'objecte que passes a
reduceres converteix internament en uncombineReducers, així queestat.reservesés el que retornireductorReserves. - Configura el middleware per defecte, inclòs el necessari per als thunks i les comprovacions de desenvolupament de l'apartat 10.
- Connecta Redux DevTools en desenvolupament, i les desconnecta en producció.
Les claus de l'objecte reducer són la forma de l'estat. Triar-les és una decisió de disseny: reserves, cataleg i sessio són els tres dominis d'estat de client que van sortir de l'auditoria de 07-01.
Els slices provisionals
Com que els slices complets són cosa de 07-04, de moment n'hi ha prou amb esquelets que permetin arrencar l'aplicació. Així queda un d'ells:
// src/funcionalitats/sessio/sliceSessio.js — VERSIÓ PROVISIONAL de 07-03
import { createSlice } from '@reduxjs/toolkit';
const sliceSessio = createSlice({
name: 'sessio',
initialState: { usuari: null, carregant: true },
reducers: {} // s'omple a 07-04
});
export default sliceSessio.reducer;L'únic que importa avui: createSlice retorna un objecte el .reducer del qual és la funció que espera configureStore. Amb reducers: {} el slice encara no accepta cap acció, però el magatzem ja té la forma correcta i DevTools te la pot ensenyar. Els altres dos —sliceCataleg i sliceReserves— són idèntics amb el seu initialState corresponent.
Comprovar que el magatzem existeix
Sense connectar res a React encara, ho pots comprovar des de la consola del navegador o des d'un fitxer de prova:
import { magatzem } from './magatzem/magatzem.js';
console.log(magatzem.getState());
// { reserves: {…}, cataleg: { tipus: 'todos', terme: '', ordre: 'model' }, sessio: { usuari: null, carregant: true } }
magatzem.dispatch({ type: 'prova/accioInventada' });
// No falla: cap reductor la reconeix i cadascun retorna el seu estat sense canvis.
// Però SÍ que apareix a DevTools, que és el que interessa ara.Aquest últim detall és una diferència de fons amb el reductorReserves de 05-05: allà una acció desconeguda llançava un error a propòsit. A Redux, cada acció passa per tots els reductors, així que una acció que un reductor no coneix és la situació normal i ha de retornar l'estat intacte. RTK ho fa per tu.
L'API del magatzem, per tenir-la completa encara que a la pràctica l'usis poc des de components:
| Mètode | Què fa |
|---|---|
magatzem.getState() |
Retorna l'estat complet actual |
magatzem.dispatch(accio) |
Envia una acció |
magatzem.subscribe(fn) |
Registra un oient que es crida després de cada acció; retorna la funció per donar-se de baixa |
react-redux usa aquests tres mètodes per sota. Des dels components usaràs useSelector i useDispatch (07-05), no aquests.
- El middleware per defecte i les seves comprovacions de desenvolupament
Un middleware és una funció que s'interposa entre dispatch i el reductor. És el punt únic d'intercepció que el context no tenia: per allà passen totes les accions, així que allà es registra, es mesura, es transforma o s'atura.
configureStore n'instal·la tres per defecte:
| Middleware | Què fa | Actiu a |
|---|---|---|
thunk |
Permet despatxar funcions a més d'objectes: la base de l'asincronia (07-04) | Desenvolupament i producció |
serializableCheck |
Avisa si fiques a l'estat o en una acció alguna cosa no serialitzable: Date, Map, Set, funcions, promeses, classes |
Només desenvolupament |
immutableCheck |
Avisa si algun reductor muta l'estat en lloc de retornar-ne un de nou | Només desenvolupament |
Les dues comprovacions són la xarxa de seguretat dels principis 2 i 3, i mereixen un exemple cadascuna.
// ⚠️ Provoca l'avís de serializableCheck
despatxar({ type: 'reserves/reservaCreada', payload: { dataInici: new Date() } });
// A non-serializable value was detected in an action, in the path: `payload.dataInici`
// ✅ Correcte: cadenes ISO, no objectes Date
despatxar({ type: 'reserves/reservaCreada', payload: { dataInici: '2026-05-04T09:00' } });Per què importa: si l'estat és serialitzable, es pot bolcar a JSON, adjuntar a un informe de fallo, persistir a localStorage i reproduir a DevTools. Un Date dins de l'estat trenca les quatre coses. Guarda cadenes ISO —que és justament el que fa el cànon de Reserva, amb dataInici: '2026-05-04T09:00'— i converteix a Date en el moment de formatar.
// ⚠️ Provoca l'avís de immutableCheck: muta l'estat FORA d'un slice
function reductorIncorrecte(estat, accio) {
estat.reserves.push(accio.payload); // mutació real
return estat;
}
// A state mutation was detected between dispatches, in the path: `reserves.reserves`Un avís important que es resol del tot a 07-04: dins d'un createSlice, escriure estat.reserves.push(...) és correcte i no dispara aquest avís, perquè Immer et lliura un esborrany i no l'estat real. Fora d'un slice, és una mutació de veritat. La diferència s'explica a fons a la propera lliçó.
Les dues comprovacions costen temps: recorren l'estat sencer després de cada acció. Amb estats grans es nota en desenvolupament, i per això RTK les desactiva en producció automàticament. Si en desenvolupament et molesten sobre una dada concreta:
export const magatzem = configureStore({
reducer: { reserves: reductorReserves, cataleg: reductorCataleg, sessio: reductorSessio },
middleware: (obtenirPerDefecte) =>
obtenirPerDefecte({
serializableCheck: { ignoredPaths: ['reserves.controlador'] }
})
});Fixa't en la signatura: middleware rep una funció que retorna la llista per defecte, i tu l'ajustes. Si escrius middleware: [elMeuMiddleware] et carregues thunk i les comprovacions de cop, que és un error freqüent i difícil de diagnosticar.
Afegir un middleware propi es fa concatenant:
const registre = (magatzem) => (seguent) => (accio) => {
console.groupCollapsed(accio.type);
console.log('abans:', magatzem.getState());
const resultat = seguent(accio);
console.log('després:', magatzem.getState());
console.groupEnd();
return resultat;
};
export const magatzem = configureStore({
reducer: { /* … */ },
middleware: (obtenirPerDefecte) => obtenirPerDefecte().concat(registre)
});Aquestes tres fletxes imbricades són la signatura canònica del middleware de Redux: rep el magatzem, retorna una funció que rep el següent baula de la cadena i retorna la que rep l'acció. A la pràctica no n'escriuràs gaires, però és útil reconèixer la forma. I aquest exemple concret és innecessari: DevTools ja et dona això mateix i millor.
- Proveir el magatzem a
main.jsx
main.jsxreact-redux exposa <Provider store={…}>, que posa el magatzem a disposició de tot l'arbre. La pregunta interessant és on col·locar-lo respecte del que ja hi ha.
// src/main.jsx — amb el magatzem de Redux
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { RouterProvider } from 'react-router';
import { magatzem } from './magatzem/magatzem.js';
import { router } from './rutes.jsx';
import LimitError from './components/LimitError.jsx';
import { Proveidors } from './contextos/Proveidors.jsx';
import { registrarError } from './utilitats/monitoritzacio.js';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<LimitError titol="CicloUrbano no està disponible en aquest moment" alRegistrar={registrarError}>
<Provider store={magatzem}>
<Proveidors>
<RouterProvider router={router} />
</Proveidors>
</Provider>
</LimitError>
</StrictMode>
);flowchart TD
A["StrictMode"] --> B["LimitError"]
B --> C["Provider store={magatzem}"]
C --> D["Proveidors<br/>Tema + Usuari"]
D --> E["RouterProvider"]
E --> F["Disseny · rutes"]
Justificació de l'ordre, perquè cada nivell respon a un motiu:
LimitErrorfora de tot (04-05): ha de capturar també les fallades que passin en crear el magatzem o en inicialitzar els proveïdors.Providerper sobre deProveidorsi de l'enrutador. És el més important:useSelectornomés funciona dins delProvider, així que qualsevol component que arribi a usar Redux —inclosos els de les pàgines d'error de ruta— hi ha d'estar a dins. I com que el magatzem és un objecte extern que mai canvia d'identitat, col·locar-lo a dalt de tot no té cost:<Provider>no torna a publicar res.Proveidorscontinua existint. Redux no substitueix el context: el tema continuarà aContextTema(07-01 va explicar per què). I si un proveïdor necessités llegir del magatzem, hauria d'estar per dins delProvider, que és el cas.RouterProviderl'últim, com a 06-02: els proveïdors que han de sobreviure a tot, inclòs l'errorElementde la ruta arrel, van per fora.
Un detall sobre StrictMode: en desenvolupament fa que els components es renderitzin dues vegades per detectar efectes impurs. Amb Redux això no duplica accions despatxades des de gestors d'esdeveniments, però sí que pot duplicar les despatxades des d'un useEffect sense dependències correctes. Si a DevTools veus cada acció de càrrega duplicada en desenvolupament, aquesta és gairebé sempre la causa, i sol ser un senyal que l'efecte està mal escrit.
- Redux DevTools: l'argument més fort
Ja tens magatzem i encara no consumeix res. Tot i així, avui pots veure l'eina que justifica mitja lliçó.
Instal·lació: l'extensió Redux DevTools per al navegador (Chrome, Firefox o Edge). No cal configurar res més: configureStore la connecta sola en desenvolupament. Arrenca npm run dev, obre l'aplicació, obre les eines del navegador i busca la pestanya Redux.
Què t'ensenya:
| Panell | Què mostra |
|---|---|
| Llista d'accions | Totes les accions despatxades, en ordre, amb el seu type |
| Action | L'acció seleccionada completa, amb el seu payload |
| State | L'estat complet just després d'aquesta acció, com a arbre navegable |
| Diff | Només el que va canviar amb aquesta acció. El panell més útil de tots |
| Trace | La pila de crides des d'on es va despatxar, si està activada |
I el que es pot fer, no només mirar:
- Recórrer l'historial. En prémer una acció anterior, l'aplicació torna a l'estat que tenia en aquell instant. La interfície s'actualitza de veritat.
- Lliscador de reproducció. Reprodueix la sessió sencera com una pel·lícula, acció a acció.
- Saltar accions (skip). Desactiva una acció de l'historial i recalcula l'estat sense ella: «què hauria passat si aquesta acció no s'hagués despatxat?».
- Despatxar accions a mà. Escriure una acció i enviar-la al magatzem per provar una transició sense tocar la interfície.
- Exportar i importar. Guardar l'historial en un fitxer JSON i carregar-lo en una altra màquina. Un informe de fallo deixa de ser «no em funciona» i passa a ser un fitxer que reprodueix el problema exacte.
flowchart LR
A["sessio/sessioIniciada"] --> B["cataleg/tipusCanviat"]
B --> C["reserves/reservaCreada"]
C --> D["reserves/reservaConfirmada"]
D -. "saltar a l'estat després de B" .-> B
Això és el que el context no et pot donar i per això molts equips trien Redux. I és un argument honest: quan el fallo és «de vegades, en confirmar dues reserves seguides, la segona apareix cancel·lada», tenir la llista exacta d'accions i el diff de cadascuna converteix una tarda de depuració en cinc minuts.
Res d'això funciona en producció, i és deliberat: les DevTools es desconnecten a la compilació de producció per no exposar l'estat ni l'historial d'un usuari real.
- Quan NO usar Redux
Un apartat imprescindible, perquè la meitat dels problemes amb Redux vénen d'usar-lo on no tocava.
No usis Redux si:
| Situació | Què usar en el seu lloc |
|---|---|
| El teu «estat global» són en realitat dades d'una API | TanStack Query o RTK Query (07-06) |
| Només necessites evitar props de pas per a sessió, tema o idioma | Context (07-02) |
| L'estat és local d'un component | useState |
| L'estat és complex però d'un sol domini i un subarbre | useReducer + context |
| La dada ha de poder compartir-se per enllaç | La URL, amb useSearchParams (06-02) |
| És un projecte petit d'una o dues persones | Context, o Zustand si vols un magatzem sense cerimònia |
| Estàs començant i encara no saps quin estat tindràs | Comença amb useState i puja segons calgui |
Sí que és una bona elecció quan es compleixen diverses d'aquestes alhora: l'aplicació és gran i creixerà; hi ha diverses persones tocant el mateix estat i fa falta una convenció; la lògica d'estat té regles de negoci que val la pena auditar i provar; necessites depurar fallos difícils de reproduir; o vols interceptar canvis en un únic punt per registrar, persistir o mesurar.
A CicloUrbano, honestament: una aplicació d'aquesta mida funcionaria perfectament amb context + useReducer, i així ha funcionat fins ara. S'introdueix Redux perquè és el que et trobaràs a la feina, perquè el model mental es transfereix a Zustand i a Jotai, i perquè les DevTools són una eina que convé tenir a les mans almenys una vegada. El que no farem és fingir que era imprescindible.
Errors Comuns i Consells
Error 1: instal·lar redux i redux-thunk a més de RTK. Vénen a dins. Instal·lar-los a part pot ficar dues còpies de Redux al paquet final i provocar fallos incomprensibles de «dos magatzems diferents».
Error 2: substituir l'array de middleware en lloc de concatenar. middleware: [elMeuMiddleware] elimina thunk i les dues comprovacions. La manera correcta és sempre (obtenirPerDefecte) => obtenirPerDefecte().concat(elMeuMiddleware).
Error 3: guardar objectes Date, Map, Set o instàncies de classe a l'estat. Trenquen la serialitzabilitat, i amb ella el viatge en el temps, l'exportació de l'historial i la persistència. Cadenes ISO i objectes plans.
Error 4: col·locar el Provider dins del RouterProvider. Qualsevol component fora d'ell —començant per l'errorElement de la ruta arrel— fallarà en usar useSelector amb un error poc descriptiu. El magatzem va a dalt.
Error 5: crear més d'un magatzem. Redux està dissenyat per a un de sol per aplicació. Si sents la necessitat de crear-ne un segon, el que vols és un altre slice.
Error 6: escriure el default d'un reductor llançant un error, per analogia amb el reductorReserves de 05-05. A Redux cada acció passa per tots els reductors, així que llançar trencaria l'aplicació a la primera acció d'un altre domini.
Consell 1: crea el magatzem abans que els slices. Comença amb esquelets buits, arrenca l'aplicació, obre DevTools i comprova que veus l'estat inicial. Així el primer slice real s'escriu amb el bastiment ja verificat.
Consell 2: usa l'extensió des del primer dia. No la instal·lis quan tinguis un fallo: per llavors ja hauràs perdut l'historial de com hi vas arribar.
Consell 3: exporta el magatzem com a constant amb nom (export const magatzem), no per defecte. És un objecte únic de l'aplicació i convé que es digui igual a tots els fitxers.
Exercicis
Exercici 1. Crea el magatzem de CicloUrbano des de zero: instal·la els paquets, escriu els tres slices provisionals amb l'estat inicial que correspongui a cada domini segons l'auditoria de 07-01, munta src/magatzem/magatzem.js, col·loca el Provider a main.jsx i comprova a Redux DevTools que l'estat inicial té la forma esperada. Escriu l'estat inicial que has triat per a cadascun i justifica'l.
Exercici 2. Aquest configureStore té tres problemes. Troba'ls i corregeix-lo.
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import { registre } from './middlewares/registre.js';
export const magatzem = configureStore({
reducer: reductorReserves,
middleware: [thunk, registre],
preloadedState: {
sessio: { usuari: { id: 'usr-01' }, entradaEn: new Date() }
}
});Exercici 3. Per a cadascuna d'aquestes cinc dades d'una futura ampliació de CicloUrbano, decideix si va al magatzem de Redux o no, i justifica-ho amb l'arbre de decisió de 07-01 i amb l'apartat 13.
- La llista d'incidències obertes del taller, que arriba de
GET /incidencias. - L'idioma de la interfície, triat per l'usuari i usat per tota l'aplicació.
- La posició del lliscador de preu màxim del catàleg, mentre l'usuari l'arrossega.
- L'historial de les últimes cinc bicicletes consultades, que es mostra a la capçalera i persisteix entre sessions.
- Si el
DialegReservaestà obert.
Solucions
Solució 1.
// src/funcionalitats/cataleg/sliceCataleg.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceCataleg = createSlice({
name: 'cataleg',
initialState: {
tipus: 'todos', // 'todos' | 'urbana' | 'electrica' | 'carga'
terme: '', // text del cercador
ordre: 'model' // 'model' | 'preu'
},
reducers: {}
});
export default sliceCataleg.reducer;// src/funcionalitats/reserves/sliceReserves.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceReserves = createSlice({
name: 'reserves',
initialState: {
entitats: {}, // reserves per id
ids: [], // ordre d'aparició
estatCarrega: 'inactiu', // 'inactiu' | 'carregant' | 'correcte' | 'error'
error: null
},
reducers: {}
});
export default sliceReserves.reducer;// src/funcionalitats/sessio/sliceSessio.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceSessio = createSlice({
name: 'sessio',
initialState: { usuari: null, carregant: true },
reducers: {}
});
export default sliceSessio.reducer;Justificació de cada estat inicial:
cataleg: els tres criteris de filtratge, tots amb un valor neutre.tipuscomença en'todos'per coincidir amb el que ja feiaPaginaCatalegquan no hi ha?tipo=a la URL. Compte: que existeixitipusaquí no significa que el filtre deixi de viure a la URL; la relació entre tots dos es resol a 07-05.reserves: forma normalitzada (entitats+ids), perquè les reserves es busquen per identificador constantment. Es detalla a 07-04. Més un parellestatCarrega/errorper a la càrrega asíncrona, amb'inactiu'perquè encara no s'ha demanat res.sessio:usuari: nullperquè ningú ha entrat, icarregant: trueperquè en arrencar cal comprovar si existeix una sessió desada —exactament elcarregantSessiode 06-05—. Començar enfalseprovocaria queRutaProtegidaexpulsi un usuari amb sessió vàlida durant el primer render.
A DevTools, amb el magatzem muntat, el panell State ha de mostrar les tres claus i cap acció excepte la d'inicialització de Redux (@@INIT).
Solució 2. Els tres problemes:
reducer: reductorReservespassa un únic reductor com a reductor arrel. L'estat seria directament el de reserves, sense les clausreserves,catalegisessio. Ha de ser un objecte amb el mapa de reductors.middleware: [thunk, registre]substitueix la llista per defecte, així que es perdenserializableCheckiimmutableCheck. Ithunkhi sobra: RTK ja l'inclou, i la líniaimport thunk from 'redux-thunk'sobra completament.entradaEn: new Date()fica un objecteDateno serialitzable a l'estat precarregat. Ha de ser una cadena ISO.
import { configureStore } from '@reduxjs/toolkit';
import { registre } from './middlewares/registre.js';
import reductorReserves from '../funcionalitats/reserves/sliceReserves.js';
import reductorCataleg from '../funcionalitats/cataleg/sliceCataleg.js';
import reductorSessio from '../funcionalitats/sessio/sliceSessio.js';
export const magatzem = configureStore({
reducer: {
reserves: reductorReserves,
cataleg: reductorCataleg,
sessio: reductorSessio
},
middleware: (obtenirPerDefecte) => obtenirPerDefecte().concat(registre),
preloadedState: {
sessio: { usuari: { id: 'usr-01' }, entradaEn: '2026-05-04T09:00:00.000Z', carregant: false }
}
});preloadedState serveix per restaurar un estat desat o per fixar un punt de partida a les proves, i ha de respectar la mateixa forma que el mapa de reductors.
Solució 3.
| Dada | A Redux? | Justificació |
|---|---|---|
| 1. Incidències del taller | No | És estat del servidor. La primera pregunta de l'arbre de 07-01 es respon afirmativament i aquí acaba el recorregut: va a una memòria cau de consultes (07-06). Ficar-lo en un slice obliga a escriure a mà càrrega, error, cancel·lació, revalidació i invalidació |
| 2. Idioma de la interfície | No cal | Molts lectors, canvi poquíssim freqüent: el perfil exacte del context, com el tema. Si el projecte ja usa Redux per a altres coses, posar-lo en un slicePreferencies tampoc és un error; simplement no aporta res |
| 3. Lliscador de preu, mentre s'arrossega | No | Canvia desenes de vegades per segon. Cada moviment seria una acció a l'historial de DevTools, que quedaria inservible, i un cicle de comparació per a tots els subscriptors. Estat local amb useDebounce; i quan l'usuari deixi anar, el valor definitiu pot anar a la URL junt a ?tipo= |
| 4. Historial de bicicletes consultades | Sí | És estat de client genuí: el llegeix un component llunyà (la capçalera), l'escriu un altre (PaginaFitxaBicicleta), té una regla pròpia («les últimes cinc, sense repetides») que mereix un reductor i una prova, i la seva persistència es resol netament amb un middleware que escriu a localStorage després de cada acció |
5. DialegReserva obert |
No | Estat local d'interfície, l'exemple canònic de l'apartat 13 i de la taula de 07-01. Va en un useAlternar dins del component que l'obre |
El cas 4 és el més interessant perquè és l'únic que guanya de debò amb Redux, i per un motiu que no és «l'usen diversos components» —això també ho cobriria el context— sinó la combinació de tenir una regla de negoci pròpia i necessitar persistència en un punt únic: exactament les dues coses que l'apartat 13 posa a la columna de «sí».
Conclusió
Redux és un contenidor d'estat predictible, i el que hi compres no és velocitat sinó predictibilitat i capacitat de diagnòstic. Els seus tres principis —font única de veritat en un únic objecte, estat de només lectura que només canvia despatxant accions, i canvis mitjançant funcions pures— produeixen junts una propietat molt concreta: estat inicial més llista d'accions igual a estat actual, sempre. D'aquí surten el viatge en el temps, la reproducció de fallos, el registre automàtic i unes proves que són cridar una funció i comparar la sortida. El model és exactament el useReducer de 05-05 a una altra escala: les diferències són que l'estat viu fora de l'arbre de React, que el camp s'anomena type i la càrrega útil payload, que hi ha un punt únic d'intercepció —el middleware— i que hi ha eines.
Has muntat el magatzem amb Redux Toolkit, que és la manera oficial d'usar Redux avui: configureStore combina els reductors, instal·la thunk i les comprovacions de desenvolupament de serialitzabilitat i mutació, i connecta les DevTools sense que li ho hagis de demanar. El magatzem de CicloUrbano viu a src/magatzem/magatzem.js amb tres dominis —reserves, cataleg i sessio—, avui amb slices provisionals, i es proveeix amb <Provider store={magatzem}> col·locat per sobre de Proveidors i del RouterProvider, perquè useSelector només funciona a dins i perquè el magatzem, en ser un objecte extern d'identitat fixa, no costa res a dalt de tot. Del Redux clàssic —createStore, constants de tipus, switch a mà, combineReducers explícit, applyMiddleware(thunk)— només et quedes amb la capacitat de llegir-lo en projectes antics; no tornarà a aparèixer com a codi proposat. I saps quan no usar Redux, que en aquesta lliçó ocupa una taula sencera: dades de servidor, preferències ambientals, estat local, valors que han de viatjar en un enllaç i projectes petits tenen respostes millors.
Ara el magatzem està buit. El que falta és el que li dona valor: modelar l'estat i les seves transicions. A la propera lliçó escriuràs els slices de veritat amb createSlice, entendràs per què Immer et deixa escriure estat.reserves.push(...) sense mutar res, generaràs identificadors i dates fora del reductor amb prepare, normalitzaràs les entitats per id, declararàs els selectors junt al seu slice i resoldràs la càrrega asíncrona amb createAsyncThunk i els seus tres estats. La propera lliçó és Redux: Accions i Reductors.
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
