El magatzem de CicloUrbano ja té forma i comportament: tres slices complets, regles de negoci als reductors, entitats normalitzades, selectors junt al seu domini i càrrega asíncrona amb createAsyncThunk. Tot això existeix avui sense que cap component ho sàpiga. Aquesta lliçó tanca el circuit amb react-redux, i ho fa prestant una atenció desproporcionada a un sol assumpte, perquè és el que separa una integració que funciona d'una que repinta l'aplicació sencera amb cada acció: la regla d'or de la comparació per identitat a useSelector. Veuràs com se subscriu un component i quan es torna a executar, reproduiràs la fallada del selector que retorna un array nou i la corregiràs de quatre formes diferents, reescriuràs PaginaCataleg, PaginaReserves, PanellReserves i MenuUsuari sobre el magatzem, decidiràs amb arguments què es queda fora de Redux, despatxaràs thunks mostrant càrrega i error, aprendràs a llegir connect en codi heretat, depuraràs amb l'historial de DevTools i organitzaràs el codi per funcionalitat. Es tanca amb una comparació honesta entre context + reductor, Redux Toolkit i Zustand per a una aplicació com aquesta.
Contingut
useSelector: com se subscriu un component- La regla d'or: comparació per identitat
- La fallada reproduïda
- Les quatre solucions
createSelectora la pràcticauseDispatchi per quèdespatxarés estableMenuUsuarisobresliceSessioPaginaCataleg: el filtre de la URL i el magatzem convivintPaginaReservesiPanellReservessobresliceReserves- Despatxar thunks des d'un component
- Què es queda fora de Redux
connectimapStateToProps: només per llegir codi heretat- Depuració amb Redux DevTools
- Organització del codi: carpeta per funcionalitat
- Comparativa final honesta
useSelector: com se subscriu un component
useSelector: com se subscriu un componentimport { useSelector } from 'react-redux';
function DistintiuReserves() {
const quantitat = useSelector((estat) => estat.reserves.ids.length);
return <span className="distintiu">{quantitat}</span>;
}El que fa useSelector per dins, pas a pas:
- Se subscriu al magatzem mitjançant
magatzem.subscribe(...), usant elProviderque vas col·locar amain.jsx(07-03). - Executa la funció selectora amb l'estat actual i retorna el resultat.
- Després de cada acció despatxada, torna a executar la funció selectora amb l'estat nou.
- Compara el resultat nou amb l'anterior usant
Object.is(comparació per identitat de referència). - Si són diferents, provoca un repintat del component. Si són iguals, no fa res.
Aquest pas 5 és la selecció granular que el context no podia donar (07-02). Un component subscrit a estat.reserves.ids.length no es repinta quan canvia el tema, quan s'escriu al cercador o quan es confirma una reserva sense alterar el número.
I el pas 3 és el que cal tenir sempre present: la funció selectora s'executa després de cada acció, sigui del domini que sigui. sessio/sessioIniciada, cataleg/termeCanviat, reserves/reservaConfirmada: totes fan que s'executin tots els selectors de tots els components muntats. Els selectors han de ser, per tant, barats.
flowchart TD
A["dispatch(qualsevolAccio)"] --> B["El reductor produeix<br/>l'estat nou"]
B --> C["El magatzem notifica<br/>a TOTS els subscriptors"]
C --> D["Cada useSelector executa<br/>la seva funció selectora"]
D --> E{"Object.is(nou, anterior)"}
E -- "true" --> F["No hi ha repintat"]
E -- "false" --> G["Repintat del component"]
- La regla d'or: comparació per identitat
useSelectorcompara ambObject.is. Si el teu selector construeix un objecte o un array nou a cada crida, la comparació sempre donarà «ha canviat» i el component es repintarà amb cada acció del magatzem, vingui d'on vingui.
És el mateix parany d'identitat que vas veure a 07-02 amb el value d'un proveïdor, en un altre lloc i amb un altre nom. I aquí és pitjor, perquè un component mal subscrit es repinta amb accions que no tenen res a veure amb ell.
| El que retorna el selector | Es repeteix la identitat? | Conseqüència |
|---|---|---|
Un número, una cadena, un booleà, null |
Sí, si el valor és igual | ✅ Correcte |
Una referència guardada a l'estat (estat.sessio.usuari) |
Sí, mentre el reductor no la substitueixi | ✅ Correcte |
Un objecte literal { a, b } |
No, mai | ❌ Repintat a cada acció |
El resultat de .map(), .filter(), .sort(), .slice() |
No, mai | ❌ Repintat a cada acció |
Object.values(...), [...alguna cosa] |
No, mai | ❌ Repintat a cada acció |
La fila de «una referència guardada a l'estat» explica per què la normalització de 07-04 és tan bona idea: com que Immer reutilitza les referències de les branques que no canvien, estat.reserves.entitats['res-01'] és literalment el mateix objecte mentre ningú toqui aquesta reserva, i la comparació per identitat funciona sola.
- La fallada reproduïda
Anem a veure el problema amb el selector derivat de 07-04, que és exactament el cas perillós.
// ❌ VERSIÓ AMB FALLADA
import { useSelector } from 'react-redux';
import { seleccionarBicicletesVisibles } from '../funcionalitats/cataleg/selectors.js';
function PaginaCataleg() {
const visibles = useSelector(seleccionarBicicletesVisibles);
console.log('PaginaCataleg s\'ha executat');
return <LlistaBicicletes bicicletes={visibles} />;
}Recorda el selector: acaba amb [...filtrades].sort(...). Retorna un array nou cada vegada.
Prova a fer això al navegador amb DevTools obert:
- Escriu una lletra al cercador →
cataleg/termeCanviat→ s'executa el component. Correcte: el catàleg ha canviat. - Prem el botó de tema... i aquí no passa res, perquè el tema no és a Redux. Ben pensat.
- Inicia sessió →
sessio/sessioIniciada→ s'executaPaginaCataleg. El catàleg no ha canviat en absolut. - Confirma una reserva →
reserves/reservaConfirmada→ s'executaPaginaCataleguna altra vegada.
Amb el registre a la consola veuràs la línia aparèixer amb cada acció de l'aplicació. I no és només un render: com que visibles és un array nou, LlistaBicicletes rep una prop nova i també es repinta, amb totes les seves TargetaBicicleta a dins.
En desenvolupament, react-redux t'avisa per consola:
Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders.
És un avís molt fàcil d'ignorar i molt car d'ignorar.
- Les quatre solucions
Solució 1: seleccionar valors primitius
La més simple, la més ràpida i la que s'oblida més sovint.
// ❌ Retorna un objecte nou
const { nom, rol } = useSelector((estat) => ({
nom: estat.sessio.usuari?.nom,
rol: estat.sessio.usuari?.rol
}));
// ✅ Dues primitives, cada una comparable per valor
const nom = useSelector((estat) => estat.sessio.usuari?.nom);
const rol = useSelector((estat) => estat.sessio.usuari?.rol);Quan el que necessites són dos o tres valors solts, diversos useSelector de primitives sempre guanyen. No hi ha memorització que mantenir, no hi ha dependències que declarar i cadascun només repinta quan el seu valor canvia de veritat.
Solució 2: dividir en diversos useSelector
La generalització de l'anterior. Un component pot tenir tants useSelector com necessiti; el cost de cada subscripció és menyspreable.
function PanellReserves() {
const ids = useSelector(seleccionarIdsReserves); // array guardat a l'estat
const estatEnviament = useSelector(seleccionarEstatEnviament); // cadena
const error = useSelector(seleccionarErrorReserves); // cadena o null
// …
}Aquí ids és segur perquè és l'array que viu a l'estat, no un de construït al vol. Immer només el substitueix quan s'afegeix o es treu una reserva.
Solució 3: memoritzar amb createSelector
Quan el selector ha de calcular de veritat —filtrar, ordenar, agregar—, la resposta és memoritzar-lo. És l'apartat 5 sencer.
Solució 4: funció d'igualtat personalitzada
useSelector accepta un segon argument: una funció que decideix si dos resultats són iguals.
import { useSelector, shallowEqual } from 'react-redux';
// Compara les propietats de primer nivell en lloc de la referència
const { nom, rol } = useSelector(
(estat) => ({ nom: estat.sessio.usuari?.nom, rol: estat.sessio.usuari?.rol }),
shallowEqual
);shallowEqual compara clau a clau amb Object.is. L'objecte continua sent nou a cada crida, però com que el seu contingut superficial és igual, useSelector no repinta.
RTK ofereix a més useSelector amb createSelector ja integrat, i react-redux exporta helpers amistosos amb useDebugValue, però a la pràctica n'hi ha prou amb aquestes quatre opcions.
Quina triar:
| Situació | Solució |
|---|---|
| Necessites un o diversos valors solts | 1 i 2: primitives, un useSelector per valor |
| Necessites una referència que ja és a l'estat | Selecciona-la directament: és segura |
| Necessites un càlcul (filtrar, ordenar, agregar) | 3: createSelector |
| Necessites un objecte agrupat i no vols memoritzar | 4: shallowEqual |
Ordre de preferència: 1 → 2 → 3 → 4. La quarta és l'última perquè shallowEqual recorre l'objecte a cada comparació i no evita la feina del selector, només el repintat.
createSelector a la pràctica
createSelector a la pràcticacreateSelector ve de Reselect i RTK el reexporta. Rep una llista de selectors d'entrada i una funció combinadora, i memoritza: si les entrades són idèntiques a les de la crida anterior, retorna el resultat anterior sense recalcular i amb la mateixa referència.
// src/funcionalitats/cataleg/selectors.js
import { createSelector } from '@reduxjs/toolkit';
import {
seleccionarBicicletes, seleccionarTerme, seleccionarOrdre
} from './sliceCataleg.js';
/**
* Bicicletes visibles del catàleg.
* El TIPUS no surt del magatzem: arriba com a argument perquè viu a la URL.
*/
export const seleccionarBicicletesVisibles = createSelector(
[
seleccionarBicicletes, // entrada 1: estat.cataleg.bicicletes
seleccionarTerme, // entrada 2: estat.cataleg.terme
seleccionarOrdre, // entrada 3: estat.cataleg.ordre
(estat, tipus) => tipus // entrada 4: el filtre de la URL
],
(bicicletes, terme, ordre, tipus) => {
const cercat = terme.trim().toLowerCase();
const filtrades = bicicletes.filter((bici) => {
const coincideixTipus = tipus === 'todos' || bici.tipus === tipus;
const coincideixTerme = cercat === '' || bici.model.toLowerCase().includes(cercat);
return coincideixTipus && coincideixTerme;
});
return [...filtrades].sort((a, b) =>
ordre === 'preu' ? a.preuHora - b.preuHora : a.model.localeCompare(b.model)
);
}
);Què passa ara en l'escenari de l'apartat 3:
| Acció despatxada | Canvien les entrades? | Resultat |
|---|---|---|
sessio/sessioIniciada |
No | Retorna la mateixa referència. Object.is dona true. Sense repintat |
reserves/reservaConfirmada |
No | Igual. Sense repintat |
cataleg/termeCanviat |
Sí, terme |
Recalcula, array nou, repintat. Correcte |
Canvi de ?tipo= a la URL |
Sí, la quarta entrada | Recalcula. Correcte |
Tres advertències importants sobre createSelector:
1. Els selectors d'entrada han de ser barats i estables. S'executen sempre; el que s'estalvia és la funció combinadora. Si una entrada retornés un objecte nou cada vegada, la memorització mai encertaria i el selector seria pitjor que no tenir-lo.
2. La memorització té mida. Històricament Reselect guardava un sol resultat, així que un selector amb argument usat des de dos components amb arguments diferents s'invalidava mútuament a cada crida. A Reselect 5 —el que porta RTK 2— la memorització per defecte (weakMapMemoize) gestiona bé diversos arguments i aquest problema desapareix en la majoria de casos. Si necessites el comportament clàssic amb diversos consumidors, el patró és una fàbrica de selectors:
// Fàbrica: cada component crea la seva pròpia instància memoritzada
export const crearSelectorReservesDeUsuari = () =>
createSelector(
[seleccionarEntitatsReserves, seleccionarIdsReserves, (estat, usuariId) => usuariId],
(entitats, ids, usuariId) => ids.map((id) => entitats[id]).filter((r) => r.usuari === usuariId)
);
// Al component
function ReservesDeUsuari({ usuariId }) {
// useMemo manté la MATEIXA instància entre renders (08-03)
const selector = useMemo(crearSelectorReservesDeUsuari, []);
const reserves = useSelector((estat) => selector(estat, usuariId));
// …
}3. No memoritzis el que no costa. createSelector al voltant de (estat) => estat.sessio.usuari és soroll: no hi ha cap càlcul a estalviar i la referència ja és estable. Memoritza quan el selector construeixi alguna cosa.
createSelector i useMemo resolen el mateix problema en capes diferents: un memoritza sobre l'estat del magatzem i serveix per a tots els components; l'altre memoritza dins d'un component. El detall de quan compensa memoritzar és a 08-03.
useDispatch i per què despatxar és estable
useDispatch i per què despatxar és estableimport { useDispatch } from 'react-redux';
import { reservaConfirmada } from '../funcionalitats/reserves/sliceReserves.js';
function BotoConfirmar({ idReserva }) {
const despatxar = useDispatch();
function gestionarClic() {
despatxar(reservaConfirmada(idReserva));
}
return <button type="button" onClick={gestionarClic}>Confirmar</button>;
}useDispatch retorna la funció dispatch del magatzem, que és sempre la mateixa: el magatzem es crea una vegada, a magatzem.js, i el seu dispatch no canvia mai durant la vida de l'aplicació.
Conseqüències pràctiques:
- Un component que només despatxa mai es repinta per Redux. No està subscrit a res. És l'equivalent automàtic de la divisió estat/accions que a 07-02 va caler muntar a mà amb dos contextos.
despatxarpot anar a les dependències d'unuseEffectsense causar bucles. De fet, el linter de hooks et demanarà que l'incloguis, i és correcte fer-ho.- No cal memoritzar els gestors que només despatxen per por de la identitat: la funció que provoca la feina és estable.
Fixa't en el patró de noms del projecte aplicat a Redux: gestionarClic és el gestor intern del component i reservaConfirmada és el creador d'acció importat del slice. La convenció de «gestors gestionarX a dins, props alX cap a fora» continua vigent; el que canvia és que ara, en lloc d'invocar una prop alConfirmar, molts components despatxen directament.
Un matís de disseny que val la pena pensar: no tots els components haurien de despatxar. Una TargetaBicicleta que despatxa accions deixa de ser reutilitzable i deixa de poder-se provar sense un magatzem. La regla raonable és que les pàgines i els panells es connecten; els components de presentació reben props. LlistaBicicletes i TargetaBicicleta continuen rebent bicicletes i alSeleccionar com fins ara.
MenuUsuari sobre sliceSessio
MenuUsuari sobre sliceSessioEl cas més senzill, i el que ensenya la solució 1.
// src/components/MenuUsuari.jsx
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { sessioTancada } from '../funcionalitats/sessio/sliceSessio.js';
import { seleccionarUsuari, seleccionarEsOperari }
from '../funcionalitats/sessio/sliceSessio.js';
import { useAlternar } from '../hooks/useAlternar.js';
import estils from './MenuUsuari.module.css';
function MenuUsuari() {
// Primitives i referències de l'estat: comparació per identitat segura
const usuari = useSelector(seleccionarUsuari);
const esOperari = useSelector(seleccionarEsOperari);
const despatxar = useDispatch();
const navegar = useNavigate();
// Estat local d'interfície: NO va al magatzem
const [obert, alternarObert] = useAlternar(false);
function gestionarTancarSessio() {
despatxar(sessioTancada());
alternarObert();
navegar('/', { replace: true });
}
if (!usuari) {
return <Link to="/acceso" className={estils.acces}>Identifica't</Link>;
}
return (
<div className={estils.menu}>
<button type="button" onClick={alternarObert} aria-expanded={obert}>
{usuari.nom}
</button>
{obert && (
<ul className={estils.desplegable}>
{esOperari && <li><Link to="/taller">Taller</Link></li>}
<li><Link to="/reservas">Les meves reserves</Link></li>
<li><button type="button" onClick={gestionarTancarSessio}>Tancar sessió</button></li>
</ul>
)}
</div>
);
}
export default MenuUsuari;Tres punts que resumeixen mitja lliçó:
seleccionarEsOperariretorna un booleà. És estat derivat calculat al selector (07-04), i en ser primitiu la comparació per identitat funciona perfectament.obertes queda auseAlternar. És exactament l'exemple de l'apartat 11.sessioTancada()es crida sense arguments i retorna{ type: 'sessio/sessioTancada' }. I recorda de 07-04 quesliceReservesreacciona a aquesta mateixa acció al seuextraReducersnetejant les reserves: un sol despatx, dos dominis actualitzats, sense acoblament entre components.
PaginaCataleg: el filtre de la URL i el magatzem convivint
PaginaCataleg: el filtre de la URL i el magatzem convivintAquesta és la decisió de disseny més interessant de la lliçó, i estava pendent des de 07-04.
El problema: ?tipo= viu a la URL des de 06-02 i sliceCataleg té un camp tipus. Dues fonts de veritat per a la mateixa dada és exactament el que 07-01 prohibeix.
La decisió: el tipus es queda a la URL i desapareix del slice. La URL és la font de veritat perquè el filtre ha de poder compartir-se per enllaç i sobreviure a un recarregat, i cap magatzem de client dona això. El slice conserva terme i ordre, que no són enllaçables per decisió de producte, i seleccionarBicicletesVisibles rep el tipus com a argument, tal com es va escriure a l'apartat 5.
// src/pagines/PaginaCataleg.jsx — amb Redux i la URL convivint
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router';
import { useSelector, useDispatch } from 'react-redux';
import CercadorBicicletes from '../components/CercadorBicicletes.jsx';
import SelectorTipus from '../components/SelectorTipus.jsx';
import LlistaBicicletes from '../components/LlistaBicicletes.jsx';
import IndicadorDeCarrega from '../components/IndicadorDeCarrega.jsx';
import Avis from '../components/Avis.jsx';
import { carregarBicicletes, termeCanviat, seleccionarTerme,
seleccionarEstatCarregaCataleg, seleccionarErrorCataleg }
from '../funcionalitats/cataleg/sliceCataleg.js';
import { seleccionarBicicletesVisibles } from '../funcionalitats/cataleg/selectors.js';
function PaginaCataleg() {
const [parametres, establirParametres] = useSearchParams();
const despatxar = useDispatch();
// FONT DE VERITAT DEL FILTRE: la URL, com a 06-02
const tipusTriat = parametres.get('tipo') ?? 'todos';
// Del magatzem: primitives i un selector memoritzat amb argument
const terme = useSelector(seleccionarTerme);
const estatCarrega = useSelector(seleccionarEstatCarregaCataleg);
const error = useSelector(seleccionarErrorCataleg);
const visibles = useSelector((estat) => seleccionarBicicletesVisibles(estat, tipusTriat));
// Estat compartit entre pocs: es queda local (07-01)
const [idSeleccionada, setIdSeleccionada] = useState(null);
useEffect(() => {
despatxar(carregarBicicletes());
}, [despatxar]);
function gestionarCanviDeTipus(tipusNou) {
const seguents = new URLSearchParams(parametres);
if (tipusNou === 'todos') seguents.delete('tipo');
else seguents.set('tipo', tipusNou);
establirParametres(seguents, { replace: true });
}
function gestionarCercar(text) {
despatxar(termeCanviat(text));
}
if (estatCarrega === 'carregant') {
return <IndicadorDeCarrega missatge="Carregant el catàleg…" />;
}
return (
<section>
<h1>Catàleg de bicicletes</h1>
{error && <Avis to="error" text={error} />}
<CercadorBicicletes valor={terme} alCercar={gestionarCercar} />
<SelectorTipus valor={tipusTriat} alCanviar={gestionarCanviDeTipus} />
<LlistaBicicletes
bicicletes={visibles}
idSeleccionada={idSeleccionada}
alSeleccionar={setIdSeleccionada}
/>
</section>
);
}
export default PaginaCataleg;El que cal retenir d'aquest component:
- Tres fonts d'estat coexisteixen sense duplicar-se: el filtre a la URL, el terme i el catàleg al magatzem, i la selecció en un
useStatelocal. Cadascuna on li correspon segons la taxonomia de 07-01. useEffectamb[despatxar]com a única dependència funciona precisament perquèdespatxarés estable. Iconditiondel thunk (07-04) evita que el doble render deStrictModeprovoqui dues peticions.LlistaBicicletesiSelectorTipusno coneixen Redux. Continuen rebent props. Són components de presentació i així es mantenen provables i reutilitzables.- L'alternativa que NO s'ha triat: mantenir
tipusal slice i sincronitzar-lo amb la URL mitjançant unuseEffect. Hauria funcionat gairebé sempre, i hauria fallat en els casos difícils —enganxar una URL, el botó «enrere», obrir en una pestanya nova—, a més de ficar un render extra a cada canvi de filtre. Quan hi ha dos candidats a font de veritat, se'n tria un i l'altre deixa d'existir.
PaginaReserves i PanellReserves sobre sliceReserves
PaginaReserves i PanellReserves sobre sliceReserves// src/pagines/PaginaReserves.jsx
import { useSelector } from 'react-redux';
import { seleccionarIdsReserves } from '../funcionalitats/reserves/sliceReserves.js';
import PanellReserva from '../components/PanellReserva.jsx';
function PaginaReserves() {
// Referència de l'estat, no un array construït: segur
const ids = useSelector(seleccionarIdsReserves);
if (ids.length === 0) {
return <p>Encara no tens cap reserva. <Link to="/reservas/nueva">Crear-ne una</Link></p>;
}
return (
<section>
<h1>Les meves reserves</h1>
<ul>
{ids.map((id) => <PanellReserva key={id} idReserva={id} />)}
</ul>
</section>
);
}
export default PaginaReserves;// src/components/PanellReserva.jsx — cada fila se subscriu a LA SEVA reserva
import { useSelector, useDispatch } from 'react-redux';
import { reservaConfirmada, reservaCancellada, seleccionarReservaPerId }
from '../funcionalitats/reserves/sliceReserves.js';
import EtiquetaEstat from './EtiquetaEstat.jsx';
function PanellReserva({ idReserva }) {
const reserva = useSelector((estat) => seleccionarReservaPerId(estat, idReserva));
const despatxar = useDispatch();
if (!reserva) return null;
return (
<li>
<h2>{reserva.bicicletaId}</h2>
<EtiquetaEstat estat={reserva.estat} />
<p>{reserva.hores} h des de {reserva.dataInici.replace('T', ' ')}</p>
{reserva.estat === 'activa' && (
<>
<button type="button" onClick={() => despatxar(reservaConfirmada(idReserva))}>
Confirmar
</button>
<button type="button" onClick={() => despatxar(reservaCancellada(idReserva))}>
Cancel·lar
</button>
</>
)}
</li>
);
}
export default PanellReserva;Aquest parell de components il·lustra el patró més eficient de Redux amb llistes, i val la pena explicar-lo bé:
- El pare selecciona només els identificadors.
idsés l'array que viu a l'estat; només canvia de referència en afegir o treure una reserva. Confirmar una reserva no repintaPaginaReserves. - Cada fill selecciona la seva pròpia entitat per identificador. Gràcies a Immer,
entitats['res-01']conserva la seva referència mentre ningú toqui aquesta reserva, així que confirmarres-01repinta exclusivament la fila deres-01.
Amb una llista de dues-centes reserves, la diferència entre aquest patró i seleccionar l'array complet al pare és de dos-cents renders a un. És l'aplicació pràctica de per què es va normalitzar l'estat a 07-04.
- Despatxar thunks des d'un component
Un thunk es despatxa igual que una acció: despatxar(carregarBicicletes()). La diferència és que retorna una promesa amb mètodes útils.
// src/pagines/PaginaNovaReserva.jsx (fragment)
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { enviarReserva, seleccionarEstatEnviament, seleccionarErrorReserves }
from '../funcionalitats/reserves/sliceReserves.js';
import { useAvisosAccions } from '../contextos/ContextAvisos.jsx';
function PaginaNovaReserva() {
// L'esborrany és estat de FORMULARI: es queda local (07-01)
const [esborrany, setEsborrany] = useState({ bicicletaId: '', dataInici: '', hores: 1 });
const estatEnviament = useSelector(seleccionarEstatEnviament);
const error = useSelector(seleccionarErrorReserves);
const despatxar = useDispatch();
const navegar = useNavigate();
const { mostrarAvis } = useAvisosAccions();
const enviant = estatEnviament === 'enviant';
async function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
// unwrap() llança si el thunk ha acabat en rejected, i retorna el payload si no
try {
const reserva = await despatxar(enviarReserva(esborrany)).unwrap();
mostrarAvis('exit', `Reserva ${reserva.id} creada.`);
navegar('/reservas', { replace: true });
} catch (fallada) {
// L'estat del magatzem ja recull l'error; aquí només decidim la interfície
mostrarAvis('error', String(fallada));
}
}
return (
<form onSubmit={gestionarEnviament} aria-busy={enviant}>
{/* … camps del formulari … */}
{error && <p role="alert">{error}</p>}
<button type="submit" disabled={enviant}>
{enviant ? 'Enviant…' : 'Reservar'}
</button>
</form>
);
}Punts importants:
.unwrap()converteix el resultat del thunk en alguna cosa amb què es pot usartry/catch: retorna elpayloaddelfulfilledo llança l'error delrejected. Sense ell,await despatxar(...)es resol sempre, fins i tot quan la petició ha fallat, i elcatchno s'executaria mai. És la fallada més comuna en despatxar thunks.- La càrrega i l'error es llegeixen del magatzem, no d'un
useStateparal·lel. Una única font de veritat, i a més visible a DevTools. - La navegació i l'avís passen al component, no al thunk. Un reductor no navega i un thunk no hauria de conèixer l'enrutador: la lògica d'interfície es queda a la interfície.
disabled={enviant}iaria-busy(03-06) eviten el doble enviament des de la interfície; elconditiondel thunk (07-04) també l'evita des del magatzem. Les dues coses, no només una.
- Què es queda fora de Redux
Tenir un magatzem no vol dir ficar-ho tot a dins. Aquesta és la llista raonada per a CicloUrbano.
| Dada | A Redux? | Justificació |
|---|---|---|
| Tema visual | No: continua a ContextTema |
Canvia una vegada per sessió, el llegeix un botó i l'atribut data-tema del document. No hi ha lògica a auditar ni accions a traçar. Ficar-lo només afegiria soroll a l'historial |
obert de MenuUsuari, Modal, DialegReserva |
No: useAlternar local |
Estat local d'interfície. Al magatzem embrutaria DevTools amb desenes d'accions irrellevants i obligaria a comprovar qui més llegeix aquest camp abans de tocar-lo |
Filtre ?tipo= |
No: viu a la URL | Ha de poder compartir-se per enllaç i sobreviure a un recarregat. Dues fonts de veritat és pitjor que cap |
idSeleccionada del catàleg |
No: useState a PaginaCataleg |
Compartit entre dos germans i mor amb la pantalla. Elevar-lo al magatzem seria globalització prematura de manual |
esborrany del formulari |
No: useState a la pàgina |
Canvia amb cada tecla. Cada tecla seria una acció, un cicle de comparació a tots els subscriptors i una entrada a l'historial |
| Avisos | No: continuen a ContextAvisos |
Funcionen bé, estan dividits en estat i accions (07-02) i la seva vida és purament visual |
| Sessió | Sí, sliceSessio |
Diverses pantalles la llegeixen, té transicions amb regles i altres dominis reaccionen al seu tancament |
| Reserves | Sí, sliceReserves |
Regles de negoci que mereixen reductor i prova, entitats compartides entre pantalles |
terme i ordre |
Sí, sliceCataleg |
Es comparteixen entre el cercador i la llista, i han de sobreviure a anar a una fitxa i tornar |
| Catàleg de bicicletes | Avui sí, a 07-06 no | És estat del servidor. És al slice de forma provisional, tal com es va anunciar a 07-04 |
El criteri que resumeix la taula: alguna cosa entra a Redux quan el seu canvi és un succés del domini que mereix quedar registrat. «L'usuari ha obert un menú» no ho és. «L'usuari ha cancel·lat una reserva» sí.
connect i mapStateToProps: només per llegir codi heretat
connect i mapStateToProps: només per llegir codi heretat⚠️ Aquest apartat existeix únicament perquè sàpigues llegir projectes anteriors al 2019.
connectcontinua funcionant ireact-reduxel manté, però no s'usa en codi nou. No tornarà a aparèixer al curs.
Abans dels hooks, la connexió es feia amb un component d'ordre superior:
// ⚠️ CODI HERETAT — NO ESCRIURE AIXÍ AVUI
import { connect } from 'react-redux';
import { reservaConfirmada, reservaCancellada } from './accions.js';
function PanellReserva({ reserva, alConfirmar, alCancellar }) {
return (
<li>
<h2>{reserva.bicicletaId}</h2>
<button onClick={() => alConfirmar(reserva.id)}>Confirmar</button>
<button onClick={() => alCancellar(reserva.id)}>Cancel·lar</button>
</li>
);
}
// Què de l'estat es converteix en props
function mapStateToProps(estat, propsPropies) {
return { reserva: estat.reserves.entitats[propsPropies.idReserva] };
}
// Quines accions es converteixen en props ja despatxades
const mapDispatchToProps = {
alConfirmar: reservaConfirmada,
alCancellar: reservaCancellada
};
export default connect(mapStateToProps, mapDispatchToProps)(PanellReserva);Equivalència amb hooks:
| API heretada | Equivalent amb hooks | Nota |
|---|---|---|
mapStateToProps(estat) |
useSelector(selector) |
Un useSelector per valor, en lloc d'un objecte amb tot |
mapStateToProps(estat, propsPropies) |
useSelector((e) => selector(e, props.id)) |
Les props s'usen directament, sense segon paràmetre |
mapDispatchToProps com a objecte |
const despatxar = useDispatch() + despatxar(accio()) |
Ja no s'embolcallen els creadors |
mapDispatchToProps com a funció |
Igual: despatxar(...) al gestor |
|
mergeProps |
Combinar al mateix component | Es feia servir poques vegades |
connect()(Component) |
Res: el component usa els hooks | Desapareix un nivell d'embolcall |
ownProps |
Les props normals del component |
Per què es va abandonar: connect afegeix un component embolcallador per cada connexió —cosa que embruta l'arbre a DevTools—, obliga a separar el component «beneit» del «connectat», té una API amb quatre formes diferents d'escriure mapDispatchToProps i és notablement difícil de tipar. Els hooks fan el mateix amb menys indirecció.
El que sí convé conservar d'aquella època és la distinció conceptual entre components de presentació i connectats. connect la imposava; amb hooks cal mantenir-la per disciplina, i és la regla de l'apartat 6: pàgines i panells es connecten, els components de presentació reben props.
- Depuració amb Redux DevTools
Ara que l'aplicació despatxa accions de veritat, l'eina de 07-03 val el que prometia. Un exemple concret de com canvia la manera de buscar una fallada.
El símptoma: «de vegades, en confirmar dues reserves seguides, la segona apareix cancel·lada».
Sense Redux, la investigació seria: posar console.log a tres gestors, intentar reproduir-ho, sospitar d'un problema de tancament (closure), sospitar d'una condició de cursa, afegir més registres.
Amb Redux DevTools:
- Reprodueix la fallada una vegada.
- Mira la llista d'accions. Aquí hi ha tota la seqüència real:
sessio/sessioIniciada cataleg/carregarBicicletes/pending cataleg/carregarBicicletes/fulfilled reserves/reservaConfirmada ← payload: 'res-01' reserves/reservaCancellada ← payload: 'res-02' ⚠️ d'on surt això? reserves/reservaConfirmada ← payload: 'res-02' - L'acció hi sobra i està identificada en vint segons. La fallada no és al reductor ni a l'estat: hi ha un
onClickque despatxareservaCancelladaquan no hauria de fer-ho, probablement un gestor mal cablejat al botó de la segona fila. - Prem skip sobre aquesta acció i comprova que sense ella el resultat és el correcte: confirma el diagnòstic sense tocar el codi.
- Mira el panell Diff de l'acció sospitosa per veure exactament què ha canviat.
I si la fallada la va reportar un usuari, pot exportar l'historial en JSON i tu importar-lo: reprodueixes la seva sessió exacta a la teva màquina.
El que canvia de fons no és la velocitat, és la naturalesa de la pregunta. Sense Redux et preguntes «per què l'estat està malament?». Amb Redux et preguntes «quina acció l'ha deixat així?», i aquesta pregunta sempre té resposta, perquè és a la llista.
Tres funcions que s'usen cada dia:
| Funció | Quan |
|---|---|
| Diff | El primer que cal mirar: què ha canviat amb aquesta acció i res més |
| Jump / saltar a una acció | Tornar a l'estat anterior i veure la interfície d'aquell instant |
| Skip | Comprovar una hipòtesi sense modificar el codi |
- Organització del codi: carpeta per funcionalitat
Fins ara CicloUrbano s'organitza per tipus: components/, pagines/, hooks/, contextos/, reductors/. Amb Redux, la comunitat en recomana una altra.
| Per tipus | Per funcionalitat | |
|---|---|---|
| Agrupa | Fitxers que fan el mateix | Fitxers que parlen del mateix |
| Afegir una funcionalitat | Tocar 5 carpetes | Crear 1 carpeta |
| Esborrar una funcionalitat | Buscar per tot el projecte | Esborrar la carpeta |
| Trobar «tot el que és de reserves» | Difícil | Trivial |
| Escala bé fins a | Projectes petits | Qualsevol mida |
src/ ├── main.jsx ├── rutes.jsx ├── index.css ├── magatzem/ │ └── magatzem.js # configureStore amb el mapa de reductors ├── funcionalitats/ │ ├── reserves/ │ │ ├── sliceReserves.js # estat, reductors, thunks, selectors │ │ ├── sliceReserves.test.js │ │ ├── PaginaReserves.jsx │ │ ├── PaginaNovaReserva.jsx │ │ ├── PanellReserva.jsx │ │ ├── PanellReserva.module.css │ │ └── FormulariReserva.jsx │ ├── cataleg/ │ │ ├── sliceCataleg.js │ │ ├── selectors.js # selectors derivats amb createSelector │ │ ├── PaginaCataleg.jsx │ │ ├── PaginaFitxaBicicleta.jsx │ │ ├── LlistaBicicletes.jsx │ │ ├── TargetaBicicleta.jsx │ │ └── SelectorTipus.jsx │ ├── sessio/ │ │ ├── sliceSessio.js │ │ ├── PaginaAcces.jsx │ │ ├── MenuUsuari.jsx │ │ ├── RutaProtegida.jsx │ │ └── RequereixRol.jsx │ └── estacions/ │ ├── PaginaEstacions.jsx │ ├── PaginaDetallEstacio.jsx │ ├── PestanyaFlota.jsx │ └── PestanyaIncidencies.jsx ├── components/ # COMPARTITS: no pertanyen a un domini │ ├── Disseny.jsx │ ├── Capcalera.jsx │ ├── PeuDePagina.jsx │ ├── Panell.jsx │ ├── Modal.jsx │ ├── Avis.jsx │ ├── EtiquetaEstat.jsx │ ├── IndicadorDeCarrega.jsx │ ├── MollesDePa.jsx │ └── LimitError.jsx ├── contextos/ # tema i avisos: continuen sense Redux │ ├── ContextTema.jsx │ ├── ContextAvisos.jsx │ └── Proveidors.jsx ├── hooks/ # genèrics, sense domini │ ├── useAlternar.js │ ├── useMagatzemLocal.js │ ├── useDebounce.js │ └── useAmpladaFinestra.js ├── utilitats/ └── dades/
Regles que fan que això funcioni:
components/guarda només el genuïnament compartit. Si alguna cosa la usa un únic domini, viu a la seva carpeta.- Un domini no importa de l'interior d'un altre. Si
reservesnecessita alguna cosa decataleg, s'importa de l'arrel d'aquesta carpeta, i si comença a passar molt, la frontera entre dominis està mal traçada. rutes.jsxcontinua a l'arrel i sí que importa de diversos dominis: és, per definició, el mapa que els uneix.- La migració és gradual. Es mou un domini cada vegada, i l'aplicació continua funcionant a cada pas.
En un projecte de la mida de CicloUrbano l'organització per tipus encara aguanta. La raó per canviar no és estètica: és que la carpeta per funcionalitat fa visible l'acoblament. Quan reserves/ necessita cinc coses de cataleg/, es veu als import i és un senyal de disseny, no un detall de col·locació.
- Comparativa final honesta
Amb tot el mòdul a la mà, aquesta és la comparació per a una aplicació com CicloUrbano.
Context + useReducer |
Redux Toolkit | Zustand | |
|---|---|---|---|
| Dependències | Cap | 2 paquets, ~15-20 kB | 1 paquet, ~1 kB |
| Codi per començar | Un fitxer de context | Magatzem + un slice per domini | Un fitxer |
| Selecció granular | No: repinta tot el consumidor | Sí, amb useSelector |
Sí, amb selector |
| Eines de depuració | No | Excel·lents | DevTools de Redux via complement |
| Viatge en el temps | No | Sí | Limitat |
| Middleware | No | Sí | Sí, més senzill |
| Asincronia | Manual | createAsyncThunk |
Funcions async normals |
| Cerimònia | Mitjana | Alta | Molt baixa |
| Estat fora de React | No | Sí | Sí |
| Convenció d'equip | Cal inventar-la | Imposada i documentada | Cal inventar-la |
| Corba d'aprenentatge | Ja la coneixes | Mitjana | Baixa |
| Presència al mercat laboral | — | Molt freqüent | Creixent |
I la recomanació, dita sense adorns:
| Situació | Elecció |
|---|---|
| Projecte personal, 1 persona, aplicació petita | Context + useReducer. Ja el tens i funciona |
| Equip de 2-4, aplicació mitjana, sense lògica d'estat complexa | Zustand, o context si l'estat global és poc |
| Equip de 5+, aplicació gran, lògica de negoci a auditar | Redux Toolkit. La convenció i les eines es paguen soles |
| Aplicació l'estat de la qual és gairebé tot dades de servidor | TanStack Query i poca cosa més d'estat de client (07-06) |
| Projecte que ja usa Redux clàssic | Migrar a RTK gradualment, slice a slice |
Per a CicloUrbano, amb honestedat: és una aplicació petita amb un equip petit, i context + useReducer continuaria sent suficient. Redux s'ha introduït perquè és el que et trobaràs a la feina, perquè el model mental —accions, reductors purs, selectors, estat normalitzat— es transfereix íntegre a Zustand i a Jotai, i perquè les DevTools són una eina que convé haver usat almenys una vegada. El que no farem és fingir que era imprescindible.
Un últim avís, que és el més important del mòdul i el fil cap a la propera lliçó: quan 07-06 tregui les bicicletes, les estacions i les reserves del magatzem cap a una memòria cau d'estat de servidor, el que quedarà a Redux serà molt poc. En moltes aplicacions reals, després d'aquest moviment l'estat de client cap en dos contextos, i aquesta és una conclusió legítima a la qual cal arribar havent entès Redux, no evitant-lo.
Errors Comuns i Consells
Error 1: un selector que retorna un objecte o un array construït. L'error número u, amb diferència. Provoca repintats amb cada acció de l'aplicació. Primitives, referències de l'estat o createSelector.
Error 2: agrupar diversos valors en un objecte «per comoditat». useSelector((e) => ({ a: e.x, b: e.y })) sense shallowEqual és l'error 1 disfressat. Dos useSelector són més simples i més ràpids.
Error 3: oblidar .unwrap() en despatxar un thunk. await despatxar(thunk()) es resol sempre, així que el catch no s'executa mai i les fallades es traguen en silenci.
Error 4: seleccionar l'array complet al pare d'una llista. Selecciona els identificadors al pare i l'entitat a cada fill: de N repintats passes a un.
Error 5: connectar components de presentació. TargetaBicicleta amb useSelector deixa de poder-se reutilitzar i de poder-se provar sense magatzem. Pàgines i panells es connecten; la resta rep props.
Error 6: duplicar a Redux alguna cosa que ja viu a la URL i sincronitzar-la amb un efecte. Tria una font de veritat i esborra l'altra.
Error 7: llegir l'estat amb magatzem.getState() dins d'un component. No subscriu a res, així que el component no es repinta quan canvia. getState() és per a thunks i middleware.
Consell 1: activa l'avís de selectors inestables. react-redux ja l'emet en desenvolupament. No l'ignoris: cadascun d'aquests avisos és un component repintant-se de més.
Consell 2: anomena els selectors seleccionarX i exporta'ls des del slice. Un component no hauria d'escriure mai estat.reserves.entitats directament.
Consell 3: mesura amb el Profiler abans de memoritzar. createSelector en un selector trivial és cost sense benefici. El Mòdul 8 ensenya a mesurar-ho (08-05).
Consell 4: mantén el magatzem petit a propòsit. Cada camp que afegeixes és un camp que algú haurà d'entendre. La taula de l'apartat 11 és una eina de decisió, no una llista tancada.
Exercicis
Exercici 1. Aquest component provoca un repintat amb cada acció del magatzem, té tres problemes diferents i a més falla en enviar. Troba'ls i reescriu-lo.
function ResumFlota({ estacioId }) {
const { bicicletes, estacions } = useSelector((estat) => ({
bicicletes: estat.cataleg.bicicletes,
estacions: estat.estacions.llista
}));
const deLEstacio = useSelector((estat) =>
estat.cataleg.bicicletes.filter((b) => b.estacioId === estacioId)
);
const despatxar = useDispatch();
async function gestionarRecarregar() {
try {
await despatxar(carregarBicicletes(estacioId));
mostrarAvis('exit', 'Flota actualitzada.');
} catch (fallada) {
mostrarAvis('error', 'No s\'ha pogut actualitzar.');
}
}
return <Panell titol={`Flota (${deLEstacio.length})`}>{/* … */}</Panell>;
}Exercici 2. Escriu un selector memoritzat seleccionarResumReserves que retorni { actives, confirmades, cancellades, totalHores } a partir de l'estat normalitzat de sliceReserves, i el component DistintiuReserves que el consumeixi mostrant només el nombre d'actives. Justifica per què el component no hauria d'usar aquest selector tal qual i què faries en el seu lloc.
Exercici 3. Tradueix aquest component heretat a hooks, conservant el comportament.
class LlistaEstacions extends React.Component {
componentDidMount() {
this.props.carregar();
}
render() {
const { estacions, carregant, alSeleccionar } = this.props;
if (carregant) return <IndicadorDeCarrega />;
return (
<ul>
{estacions.map((e) => (
<li key={e.id} onClick={() => alSeleccionar(e.id)}>{e.nom}</li>
))}
</ul>
);
}
}
const mapStateToProps = (estat) => ({
estacions: estat.estacions.llista,
carregant: estat.estacions.estatCarrega === 'carregant'
});
const mapDispatchToProps = {
carregar: carregarEstacions,
alSeleccionar: estacioSeleccionada
};
export default connect(mapStateToProps, mapDispatchToProps)(LlistaEstacions);Solucions
Solució 1. Els tres problemes de rendiment i el d'enviament:
- El primer
useSelectorretorna un objecte literal. Referència nova a cada crida: repintat amb cada acció. - El segon
useSelectorretorna el resultat d'unfilter. Array nou a cada crida: el mateix problema, per segona vegada. bicicletesiestacionsse seleccionen i no s'usen. Subscripció gratuïta a dues branques de l'estat.- Falta
.unwrap():await despatxar(carregarBicicletes(...))es resol fins i tot quan el thunk acaba enrejected, així que elcatchmai s'executa i una fallada de xarxa mostraria «Flota actualitzada».
// src/funcionalitats/cataleg/selectors.js
export const crearSelectorBicicletesDeEstacio = () =>
createSelector(
[seleccionarBicicletes, (estat, estacioId) => estacioId],
(bicicletes, estacioId) => bicicletes.filter((b) => b.estacioId === estacioId)
);// src/funcionalitats/estacions/ResumFlota.jsx
import { useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { crearSelectorBicicletesDeEstacio } from '../cataleg/selectors.js';
import { carregarBicicletes } from '../cataleg/sliceCataleg.js';
import { useAvisosAccions } from '../../contextos/ContextAvisos.jsx';
function ResumFlota({ estacioId }) {
const despatxar = useDispatch();
const { mostrarAvis } = useAvisosAccions();
// Una instància memoritzada per component muntat
const selector = useMemo(crearSelectorBicicletesDeEstacio, []);
const deLEstacio = useSelector((estat) => selector(estat, estacioId));
async function gestionarRecarregar() {
try {
await despatxar(carregarBicicletes(estacioId)).unwrap(); // 4)
mostrarAvis('exit', 'Flota actualitzada.');
} catch (fallada) {
mostrarAvis('error', 'No s\'ha pogut actualitzar.');
}
}
return (
<Panell titol={`Flota (${deLEstacio.length})`}>
<button type="button" onClick={gestionarRecarregar}>Recarregar</button>
{/* … */}
</Panell>
);
}S'han eliminat les dues subscripcions inútils, el filtre es memoritza amb una fàbrica —perquè dos ResumFlota d'estacions diferents no s'invalidin entre si— i l'enviament informa correctament de la fallada.
Solució 2.
// src/funcionalitats/reserves/selectors.js
import { createSelector } from '@reduxjs/toolkit';
import { seleccionarEntitatsReserves, seleccionarIdsReserves } from './sliceReserves.js';
export const seleccionarResumReserves = createSelector(
[seleccionarEntitatsReserves, seleccionarIdsReserves],
(entitats, ids) => {
const resum = { actives: 0, confirmades: 0, cancellades: 0, totalHores: 0 };
for (const id of ids) {
const reserva = entitats[id];
if (reserva.estat === 'activa') resum.actives += 1;
if (reserva.estat === 'confirmada') resum.confirmades += 1;
if (reserva.estat === 'cancelada') resum.cancellades += 1;
if (reserva.estat !== 'cancelada') resum.totalHores += reserva.hores;
}
return resum;
}
);Per què DistintiuReserves no l'hauria d'usar tal qual: el selector retorna un objecte, i encara que createSelector el memoritza, es recalcula —i retorna un objecte nou— cada vegada que canvia qualsevol reserva. Si el distintiu només mostra el nombre d'actives, es repintaria en cancel·lar una reserva, en confirmar-ne una altra o en canviar les hores d'una tercera, encara que el nombre d'actives continuï igual.
La solució és seleccionar la primitiva:
// ✅ Només es repinta quan canvia el nombre d'actives
function DistintiuReserves() {
const actives = useSelector((estat) => seleccionarResumReserves(estat).actives);
if (actives === 0) return null;
return <span className="distintiu" aria-label={`${actives} reserves actives`}>{actives}</span>;
}El selector memoritzat fa el càlcul una sola vegada i es comparteix entre tots els consumidors; cada component extreu el primitiu que necessita i useSelector compara aquest número. És la combinació de les solucions 1 i 3, i és el patró recomanat per a selectors que retornen agregats: memoritza l'agregat, selecciona el camp.
Solució 3.
// src/funcionalitats/estacions/LlistaEstacions.jsx
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { carregarEstacions, estacioSeleccionada, seleccionarEstacions,
seleccionarEstatCarregaEstacions } from './sliceEstacions.js';
import IndicadorDeCarrega from '../../components/IndicadorDeCarrega.jsx';
function LlistaEstacions() {
// mapStateToProps → un useSelector per valor
const estacions = useSelector(seleccionarEstacions);
const carregant = useSelector((estat) => seleccionarEstatCarregaEstacions(estat) === 'carregant');
// mapDispatchToProps → useDispatch
const despatxar = useDispatch();
// componentDidMount → useEffect amb [] (més despatxar, que és estable)
useEffect(() => {
despatxar(carregarEstacions());
}, [despatxar]);
if (carregant) return <IndicadorDeCarrega missatge="Carregant estacions…" />;
return (
<ul>
{estacions.map((estacio) => (
<li key={estacio.id}>
<button type="button" onClick={() => despatxar(estacioSeleccionada(estacio.id))}>
{estacio.nom}
</button>
</li>
))}
</ul>
);
}
export default LlistaEstacions;Quatre millores que la traducció porta de propina:
- Desapareix el component embolcallador que
connectinseria a l'arbre. carregantés un booleà derivat al selector, no un objecte: comparació per identitat perfecta.<li onClick>passa a ser un<button>dins del<li>. L'original no era accessible: un<li>no rep focus ni respon al teclat (03-06). Migrar codi heretat és un bon moment per arreglar això.despatxara les dependències de l'efecte és correcte i no causa bucles, precisament per ser estable.
Conclusió
El circuit està tancat. useSelector subscriu un component al magatzem, executa la seva funció selectora després de cada acció i compara el resultat amb Object.is, d'on surt la regla d'or: un selector que construeix un objecte o un array nou a cada crida provoca repintats amb cada acció de l'aplicació, vingui del domini que vingui. Les quatre solucions, en ordre de preferència: seleccionar primitives, dividir en diversos useSelector, memoritzar amb createSelector —recordant que els selectors d'entrada han de ser barats i que la memorització només compensa quan hi ha càlcul— i, com a últim recurs, una funció d'igualtat com shallowEqual. useDispatch retorna la funció dispatch del magatzem, que és estable per sempre: un component que només despatxa mai es repinta per Redux, i despatxar pot anar a les dependències d'un efecte sense por.
CicloUrbano queda connectat amb criteri. MenuUsuari llegeix dues primitives de sliceSessio i conserva el seu desplegable a useAlternar. PaginaCataleg fa conviure tres fonts d'estat sense duplicar-ne cap: el filtre a la URL —que continua sent la font de veritat, amb el tipus passat com a argument al selector memoritzat—, el terme i el catàleg al magatzem, i la selecció en un useState local. PaginaReserves selecciona només els identificadors i PanellReserva selecciona la seva pròpia entitat, de manera que confirmar una reserva repinta exactament una fila: la recompensa directa d'haver normalitzat l'estat. Els thunks es despatxen amb .unwrap() perquè try/catch funcioni, i la càrrega i l'error es llegeixen del magatzem, no d'un useState paral·lel. I la taula de què es queda fora —tema, modals, filtre d'URL, selecció del catàleg, esborrany del formulari, avisos— resumeix el criteri de tot el mòdul: alguna cosa entra a Redux quan el seu canvi és un succés del domini que mereix quedar registrat.
També saps llegir connect, mapStateToProps i mapDispatchToProps sense escriure'ls mai, usar l'historial de DevTools per canviar la pregunta de «per què l'estat està malament?» a «quina acció l'ha deixat així?», i organitzar el codi per funcionalitat en lloc de per tipus, amb components/ reservat al genuïnament compartit. La comparativa final és honesta: per a una aplicació de la mida de CicloUrbano, context + useReducer hauria bastat; Redux Toolkit es paga sol quan hi ha equip, mida i lògica a auditar; i Zustand ocupa un terme mitjà molt raonable.
Queda la peça més grossa, i la que canviarà l'equilibri de tot l'anterior. Al magatzem hi ha avui un camp bicicletes carregat amb un createAsyncThunk, i ja s'ha dit dues vegades que hi era de forma provisional: les dades que vénen d'un servidor no són estat de l'aplicació. No et pertanyen, es queden obsoletes soles, es comparteixen entre pestanyes i usuaris i arriben tard. Tractar-les com a estat de client obliga a escriure a mà càrrega, error, cancel·lació, condicions de cursa, deduplicació, revalidació, invalidació, reintents i paginació —per a cada recurs—. A la propera lliçó muntaràs una API fictícia amb json-server, coneixeràs TanStack Query, entendràs el cicle de vida d'una dada en memòria cau, faràs mutacions amb actualització optimista i la seva reversió, reescriuràs useFetchBicicletes com useBicicletes, i fixaràs l'arquitectura final de CicloUrbano: Redux per a l'estat del client, Query per al del servidor. La propera lliçó és Estat del Servidor: Peticions, Memòria Cau i Sincronització.
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
