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

  1. Què és Redux i quin problema resol
  2. Els tres principis
  3. Per què aquests principis fan l'aplicació predictible i depurable
  4. Relació amb useReducer: mateix model, escala diferent
  5. El flux unidireccional
  6. Redux Toolkit: la manera oficial d'usar Redux avui
  7. Com era el Redux clàssic (codi heretat que cal saber llegir)
  8. Instal·lació
  9. El magatzem de CicloUrbano: src/magatzem/magatzem.js
  10. El middleware per defecte i les seves comprovacions de desenvolupament
  11. Proveir el magatzem a main.jsx
  12. Redux DevTools: l'argument més fort
  13. Quan NO usar Redux

  1. 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.

  1. 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.

magatzem.dispatch({ type: 'reserves/reservaConfirmada', payload: 'res-01' });

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.

  1. 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.

  1. Relació amb useReducer: mateix model, escala diferent

Això 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.

  1. 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:

  1. L'usuari prem «Confirmar» a PanellReserves i el gestor despatxa { type: 'reserves/reservaConfirmada', payload: 'res-01' }.
  2. 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ó.
  3. El reductor retorna un estat nou; mai modifica el que va rebre.
  4. El magatzem guarda l'estat nou i notifica els seus subscriptors.
  5. 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.

  1. 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.

  1. 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: createSlice genera el tipus a partir del nom.
  • El switch amb default: return estat és obligatori en Redux clàssic, perquè cada acció passa per tots els reductors. Compte amb la diferència respecte del reductorReserves de 05-05, on el default llanç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.
  • combineReducers explícit i applyMiddleware(thunk): tots dos desapareixen amb configureStore, que fa el mateix per dins.
  • És habitual trobar-ho junt a connect, mapStateToProps i mapDispatchToProps, 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.

  1. Instal·lació

npm install @reduxjs/toolkit react-redux

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.

  1. El magatzem de CicloUrbano: src/magatzem/magatzem.js

configureStore é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:

  1. Combina els reductors. L'objecte que passes a reducer es converteix internament en un combineReducers, així que estat.reserves és el que retorni reductorReserves.
  2. Configura el middleware per defecte, inclòs el necessari per als thunks i les comprovacions de desenvolupament de l'apartat 10.
  3. 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.

  1. 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.

  1. Proveir el magatzem a main.jsx

react-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:

  • LimitError fora de tot (04-05): ha de capturar també les fallades que passin en crear el magatzem o en inicialitzar els proveïdors.
  • Provider per sobre de Proveidors i de l'enrutador. És el més important: useSelector només funciona dins del Provider, 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.
  • Proveidors continua existint. Redux no substitueix el context: el tema continuarà a ContextTema (07-01 va explicar per què). I si un proveïdor necessités llegir del magatzem, hauria d'estar per dins del Provider, que és el cas.
  • RouterProvider l'últim, com a 06-02: els proveïdors que han de sobreviure a tot, inclòs l'errorElement de 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.

  1. 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.

  1. 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.

  1. La llista d'incidències obertes del taller, que arriba de GET /incidencias.
  2. L'idioma de la interfície, triat per l'usuari i usat per tota l'aplicació.
  3. La posició del lliscador de preu màxim del catàleg, mentre l'usuari l'arrossega.
  4. L'historial de les últimes cinc bicicletes consultades, que es mostra a la capçalera i persisteix entre sessions.
  5. Si el DialegReserva està obert.

Solucions

Solució 1.

npm install @reduxjs/toolkit react-redux
// 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. tipus comença en 'todos' per coincidir amb el que ja feia PaginaCataleg quan no hi ha ?tipo= a la URL. Compte: que existeixi tipus aquí 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 parell estatCarrega/error per a la càrrega asíncrona, amb 'inactiu' perquè encara no s'ha demanat res.
  • sessio: usuari: null perquè ningú ha entrat, i carregant: true perquè en arrencar cal comprovar si existeix una sessió desada —exactament el carregantSessio de 06-05—. Començar en false provocaria que RutaProtegida expulsi 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:

  1. reducer: reductorReserves passa un únic reductor com a reductor arrel. L'estat seria directament el de reserves, sense les claus reserves, cataleg i sessio. Ha de ser un objecte amb el mapa de reductors.
  2. middleware: [thunk, registre] substitueix la llista per defecte, així que es perden serializableCheck i immutableCheck. I thunk hi sobra: RTK ja l'inclou, i la línia import thunk from 'redux-thunk' sobra completament.
  3. entradaEn: new Date() fica un objecte Date no 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 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

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats