La lliçó anterior va acabar amb una pregunta sense resposta: si les props vénen de fora i són de només lectura, com canvia res en una aplicació React? El catàleg de CicloUrbano ja mostra bicicletes de veritat, però és una fotografia: no reacciona a res. Quan l'usuari triï un tipus de bicicleta, reservi una unitat o filtri per estació, alguna cosa ha de canviar dins de l'aplicació i provocar un nou dibuixat. Aquesta cosa és l'estat: la memòria pròpia d'un component, l'única dada que un component pot modificar i el disparador del cicle de renderitzat que vas estudiar a la lliçó 01-05. En aquesta lliçó veuràs per què una variable de JavaScript corrent no serveix, com es declara l'estat amb useState, per què l'estat està aïllat per instància, com actualitzar-lo sense perdre actualitzacions, per què cal copiar objectes i arrays en lloc de modificar-los i —potser el més important— què mereix ser estat i què no.

Contingut

  1. Què és l'estat
  2. Estat davant de props
  3. Per què una variable normal no funciona
  4. useState: declarar, llegir i actualitzar
  5. Actualitzar l'estat dispara un render
  6. L'estat és local i està aïllat per instància
  7. Actualitzacions basades en el valor anterior
  8. Immutabilitat: objectes i arrays en l'estat
  9. Què ha de ser estat i què es pot derivar
  10. CicloUrbano: SelectorTipus i el comptador de disponibles

  1. Què és l'estat

L'estat (state) és un conjunt de dades que un component guarda entre renders i que, en canviar, fa que React el torni a renderitzar.

Fixa't en les dues meitats de la definició, perquè totes dues són imprescindibles:

  • Es conserva entre renders. Una variable normal declarada dins d'un component neix i mor a cada render. L'estat sobreviu.
  • En canviar, provoca un render. No és només emmagatzematge: és emmagatzematge connectat a la pantalla.

A la lliçó 01-05 vas veure que hi ha dues coses que disparen un render: el render inicial i un canvi d'estat. Ara ja saps gestionar el segon.

Exemples d'estat a CicloUrbano:

Dada Per què és estat?
El tipus de bicicleta seleccionat al filtre L'usuari el canvia i la llista ha de reaccionar
El nombre d'hores d'una reserva en curs Canvia en prémer els botons del formulari
Si el panell de detalls està obert o tancat Canvia amb la interacció
El text escrit al cercador Canvia a cada tecla

I exemples del que no és estat:

Dada Per què no
L'array bicicletas de domini.js És una dada fixa importada; no la canvia el component
El nom de l'estació que rep la targeta Arriba per props: pertany al pare
El nombre de bicicletes disponibles Es calcula a partir de les dades; vegeu l'apartat 9

  1. Estat davant de props

Totes dues són fonts de dades per a un component, i confondre-les és l'error conceptual més freqüent en començar.

Props Estat
Origen El component pare El propi component
Qui el pot canviar Només el pare Només el propi component
Es pot modificar des de dins? No, són de només lectura Sí, amb la seva funció actualitzadora
Sobreviu entre renders? Arriben de nou a cada render Sí, React el conserva
Efecte de canviar-lo El pare es torna a renderitzar i el fill rep valors nous El component es torna a renderitzar
Visibilitat El pare les coneix Privat: ningú més el veu
Analogia Els arguments d'una funció Una variable que la funció recorda entre crides
A CicloUrbano bicicleta, estat, nomEstacio El tipus filtrat, les hores de reserva

Una regla pràctica que resol gairebé tots els dubtes:

Aquesta dada la canvia el propi component? Si sí, és estat. Si ve de fora i ell només la llegeix, és una prop.

I un matís que arribarà al Mòdul 4: quan dos components germans necessiten la mateixa dada canviant, l'estat puja al pare comú i baixa als fills com a props. Aquest patró s'anomena elevar l'estat i s'estudia a Elevar l'Estat. En aquesta lliçó tot l'estat es queda dins d'un sol component.

  1. Per què una variable normal no funciona

Provem-ho de la manera «òbvia» i veiem exactament on falla. Aquest component mostra les places lliures de l'estació Plaça Major i hauria de descomptar-ne una en prémer el botó.

// src/components/ComptadorPlaces.jsx  — VERSIÓ TRENCADA, no la facis servir
function ComptadorPlaces() {
  let lliures = 12;                // variable normal

  function ocuparPlaça() {
    lliures = lliures - 1;
    console.log('Ara hi ha', lliures, 'places lliures');
  }

  return (
    <section className="comptador-places">
      <p>Plaça Major · Places lliures: {lliures}</p>
      <button onClick={ocuparPlaça}>Ocupar una plaça</button>
    </section>
  );
}

export default ComptadorPlaces;

Executa'l i prem el botó tres vegades. La consola diu 11, 10, 9… i la pantalla continua mostrant 12.

Hi ha dues fallades independents, i entendre-les per separat és la clau de tota la lliçó:

Fallada 1: React no se n'assabenta

Modificar una variable de JavaScript és una operació privada de JavaScript. React no vigila les teves variables: no hi ha res que les connecti amb la pantalla. Sense un canvi d'estat, no hi ha disparador de render, i sense render la pantalla no s'actualitza. És exactament el que vas veure a 01-05: el render es dispara per l'arrencada o per un canvi d'estat, i aquí no n'ha passat cap dels dos.

Fallada 2: la variable es reinicia

I encara que forcéssim un render per un altre mitjà, tampoc funcionaria. Un component és una funció, i React la torna a cridar a cada render. Cada crida executa let lliures = 12; una altra vegada, des de zero. El valor anterior s'ha perdut: era una variable local d'una crida que ja havia acabat.

flowchart TD
    A["Render 1: s'executa la funció<br/>let lliures = 12"] --> B["Prems el botó<br/>lliures passa a 11 en memòria"]
    B --> C{"React se n'assabenta?"}
    C -- No --> D["No hi ha render.<br/>La pantalla continua en 12"]
    C -. "i si el forcéssim" .-> E["Render 2: la funció s'executa una altra vegada<br/>let lliures = 12 -> torna a 12"]

Conclusió: un component necessita alguna cosa que (a) React vigili per disparar el render i (b) sobrevisqui a la reexecució de la funció. Aquesta cosa és l'estat, i es declara amb useState.

  1. useState: declarar, llegir i actualitzar

useState és un hook: una funció especial de React que permet a un component de funció accedir a capacitats del motor. El Mòdul 5 estudia els hooks a fons, i useState el cobreix amb tot detall. Aquí ens quedem amb l'ús bàsic, que és el 90 % dels casos.

import { useState } from 'react';

function ComptadorPlaces() {
  const [lliures, setLliures] = useState(12);
  // …
}

Aquesta única línia conté quatre coses:

Part Què és
useState(12) Declara un tros d'estat amb valor inicial 12
lliures La variable de lectura: el valor actual de l'estat en aquest render
setLliures La funció actualitzadora: l'única manera vàlida de canviar-lo
const [a, b] = … Desestructuració d'array: useState retorna un array de dos elements

Sobre aquesta desestructuració: useState retorna [valor, funcioActualitzadora], i les claus quadrades extreuen els dos elements i els posen nom. Els noms els tries tu; la convenció universal és alguna cosa i setAlgunaCosa, en aquest ordre.

Ara la versió que funciona:

// src/components/ComptadorPlaces.jsx  — VERSIÓ CORRECTA
import { useState } from 'react';

/**
 * Comptador de places lliures d'una estació de CicloUrbano.
 * Props:
 *  - nomEstacio (cadena, opcional, per defecte 'Plaça Major')
 *  - placesInicials (número, opcional, per defecte 12)
 */
function ComptadorPlaces({ nomEstacio = 'Plaça Major', placesInicials = 12 }) {
  const [lliures, setLliures] = useState(placesInicials);

  function ocuparPlaça() {
    setLliures(lliures - 1);
  }

  return (
    <section className="comptador-places">
      <p>
        {nomEstacio} · Places lliures: {lliures}
      </p>
      <button onClick={ocuparPlaça}>Ocupar una plaça</button>
    </section>
  );
}

export default ComptadorPlaces;

Els canvis respecte a la versió trencada són només tres: importar useState, declarar l'estat i cridar setLliures en lloc d'assignar. La resta del component és idèntica. Prem el botó: ara la pantalla baixa a 11, 10, 9…

Sobre onClick: aquí l'utilitzem com a mínim necessari per provocar un canvi. Els gestors d'esdeveniments —la sintaxi completa, l'objecte d'esdeveniment, la propagació— són el contingut de Gestió d'Esdeveniments. Queda't de moment amb la regla: es passa la funció sense parèntesis.

Dues regles sobre on va useState

Els hooks tenen normes d'ús estrictes. De moment, dues:

  1. Sempre al nivell superior del component. Mai dins d'un if, un bucle, una funció imbricada o després d'un return condicional.
  2. Només dins de components (o d'hooks personalitzats). No en una funció auxiliar qualsevol.

El motiu —React identifica cada tros d'estat per l'ordre en què es declara— i la resta de les regles s'expliquen a Hooks: Introducció i Ús Bàsic.

Diversos trossos d'estat

Un component pot declarar-ne tants com necessiti, i és el més habitual:

function PanellReserva() {
  const [hores, setHores] = useState(1);
  const [tipus, setTipus] = useState('urbana');
  const [confirmada, setConfirmada] = useState(false);
  // …
}

Mantenir-los separats és preferible a posar-los en un únic objecte: cada dada s'actualitza pel seu compte i el codi queda molt més clar. Quan diversos valors canvien sempre junts i amb regles complexes, hi ha una eina específica, useReducer, que es veu a 05-05.

  1. Actualitzar l'estat dispara un render

Això connecta directament amb la lliçó 01-05. Quan crides setLliures(11), passa aquesta seqüència:

flowchart TD
    A["Prems el botó"] --> B["S'executa setLliures(11)"]
    B --> C["React marca el component<br/>com a pendent de renderitzar"]
    C --> D["React torna a cridar<br/>ComptadorPlaces()"]
    D --> E["Aquesta vegada useState retorna 11"]
    E --> F["Es produeix un arbre d'elements nou"]
    F --> G["Reconciliació: React el compara<br/>amb l'arbre anterior"]
    G --> H["Commit: només canvia el text<br/>del paràgraf al DOM"]

Tres conseqüències que cal interioritzar:

a) El valor no canvia a la línia següent. La variable lliures d'aquest render és una constant: val el que valia quan React va executar la funció. El valor nou apareix al render següent.

function ocuparPlaça() {
  console.log(lliures);      // 12
  setLliures(lliures - 1);
  console.log(lliures);      // encara és 12!
}

Això no és un error ni un retard: és coherència. Durant tot un render, tots els valors romanen fixos, la qual cosa evita tota una classe d'errors en què unes parts de la pantalla van avançades respecte a altres.

b) React agrupa les actualitzacions. Si crides diverses vegades funcions actualitzadores dins del mateix gestor, React no renderitza una vegada per crida: les agrupa i fa un sol render al final. És el que es coneix com a batching.

c) Si el valor no canvia, no hi ha render. React compara el valor nou amb l'actual. Si són iguals (amb la comparació de Object.is), s'estalvia la feina.

setLliures(12);   // si lliures ja val 12, React no renderitza

Vés amb compte amb això últim quan l'estat és un objecte: si mutes l'objecte i li passes la mateixa referència, React conclou que no ha canviat res i no renderitza. És la causa de la meitat dels «la pantalla no s'actualitza» que veuràs en la teva vida, i el motiu de l'apartat 8.

  1. L'estat és local i està aïllat per instància

Cada vegada que fas servir un component al JSX crees una instància diferent, i cada instància té el seu propi estat, completament independent.

// src/App.jsx (fragment)
<ComptadorPlaces nomEstacio="Plaça Major" placesInicials={20} />
<ComptadorPlaces nomEstacio="Parc Nord" placesInicials={15} />
<ComptadorPlaces nomEstacio="Estació Central" placesInicials={30} />

Tres comptadors en pantalla. Prem el botó del primer: baixa de 20 a 19 i els altres dos no es bellugen. Comparteixen el codi, no les dades.

flowchart TD
    APP[App] --> C1["ComptadorPlaces<br/>Plaça Major<br/>estat: lliures = 19"]
    APP --> C2["ComptadorPlaces<br/>Parc Nord<br/>estat: lliures = 15"]
    APP --> C3["ComptadorPlaces<br/>Estació Central<br/>estat: lliures = 30"]

Aquesta propietat té conseqüències importants:

  • L'estat és privat. Cap altre component el pot llegir ni escriure. Ni el pare. Aquesta encapsulació és el que fa que un component sigui raonable en aïllament.
  • Si dos components necessiten compartir-lo, l'estat és al lloc equivocat. Cal pujar-lo al pare comú (04-01).
  • L'estat va lligat a la posició a l'arbre. Si React destrueix el component —perquè canvia de tipus o desapareix de la pantalla—, el seu estat es perd. És exactament l'heurística de reconciliació que vas estudiar a 01-05, vista ara des de l'altre costat.

  1. Actualitzacions basades en el valor anterior

La funció actualitzadora admet dues formes d'ús, i la diferència importa.

setLliures(lliures - 1);              // Forma directa: passes el valor nou
setLliures(anterior => anterior - 1); // Forma funcional: passes una funció

En la forma funcional, React crida la teva funció passant-li el valor més recent de l'estat i fa servir el que retornes com a valor nou.

Per què la forma directa pot fallar

Imagina un botó que allibera tres places de cop:

function alliberarTresPlaces() {
  setLliures(lliures + 1);
  setLliures(lliures + 1);
  setLliures(lliures + 1);
}

Si lliures val 12, esperaries 15. Obtens 13. La raó: lliures és una constant fixa durant tot aquest render, amb valor 12. Les tres crides diuen literalment «posa l'estat a 13», tres vegades. I com que React agrupa les actualitzacions, el resultat final és 13.

Amb la forma funcional:

function alliberarTresPlaces() {
  setLliures(anterior => anterior + 1);   // 12 -> 13
  setLliures(anterior => anterior + 1);   // 13 -> 14
  setLliures(anterior => anterior + 1);   // 14 -> 15
}

React encua les tres funcions i les aplica en ordre, cadascuna sobre el resultat de l'anterior. Ara sí: 15.

Forma Sintaxi Quan fer-la servir
Directa setLliures(5) El valor nou no depèn de l'anterior
Funcional setLliures(prev => prev - 1) El valor nou depèn de l'anterior

Regla pràctica: si en l'argument apareix la variable d'estat, fes servir la forma funcional. Sempre és correcta, no costa res i evita una família sencera d'errors, inclosos els que només apareixen amb codi asíncron.

  1. Immutabilitat: objectes i arrays en l'estat

Aquí hi ha el parany més important de l'estat a React.

No modifiquis mai directament un objecte o un array que estigui en l'estat. Crea'n una còpia nova.

El motiu és el que vas veure a l'apartat 5: React decideix si cal renderitzar comparant referències. Si mutes l'objecte, la referència no canvia, React conclou que res no ha canviat i no renderitza.

El cas d'un objecte

Suposem que un component guarda a l'estat la bicicleta seleccionada i volem marcar-la com a reservada.

const [bicicletaSeleccionada, setBicicletaSeleccionada] = useState(bicicletas[0]);
// ❌ MALAMENT: mutació. La pantalla no s'actualitza.
function reservar() {
  bicicletaSeleccionada.estat = 'alquilada';
  setBicicletaSeleccionada(bicicletaSeleccionada);   // mateixa referència -> sense render
}
// ✅ BÉ: còpia amb el camp canviat
function reservar() {
  setBicicletaSeleccionada({
    ...bicicletaSeleccionada,
    estat: 'alquilada'
  });
}

L'operador de propagació copia totes les propietats de l'objecte original a un de nou, i estat: 'alquilada', en anar després, sobreescriu aquesta clau. El resultat és un objecte diferent, amb una referència nova, i React renderitza.

I amb la forma funcional, que és la recomanada:

function reservar() {
  setBicicletaSeleccionada(anterior => ({ ...anterior, estat: 'alquilada' }));
}

Fixa't en els parèntesis que envolten les claus: anterior => ({ … }). Sense ells, JavaScript interpreta les claus com el cos de la funció fletxa i retorna undefined. És una errada clàssica.

El cas d'un array

Un component que guarda una llista de bicicletes en el seu estat:

const [flota, setFlota] = useState(bicicletas);

Els mètodes d'array es divideixen en dos grups, i aquesta taula val la pena tenir-la a mà:

Operació ❌ Muta (prohibit) ✅ Retorna còpia (correcte)
Afegir al final push [...flota, nova]
Afegir al principi unshift [nova, ...flota]
Eliminar splice, pop, shift flota.filter(b => b.id !== id)
Reemplaçar un element flota[0] = … flota.map(b => b.id === id ? nova : b)
Ordenar sort [...flota].sort(…)
Invertir reverse [...flota].reverse()

Marcar una bicicleta com a reservada dins d'un array combina les dues idees: cal copiar l'array i copiar l'objecte que canvia.

// ✅ Còpia de l'array (map) + còpia de l'objecte modificat (spread)
function reservarBicicleta(id) {
  setFlota(anterior =>
    anterior.map(bici =>
      bici.id === id
        ? { ...bici, estat: 'alquilada' }   // objecte nou només per a aquesta
        : bici                               // les altres, tal qual
    )
  );
}

// En cridar reservarBicicleta('bici-001'):
// - flota és un array NOU (map sempre en crea un)
// - bici-001 és un objecte NOU amb estat 'alquilada'
// - bici-002 ... bici-005 són EXACTAMENT els mateixos objectes d'abans

Aquest últim detall és elegant i deliberat: els elements que no canvien conserven la seva referència, la qual cosa permet a React —i a les optimitzacions del Mòdul 8— estalviar-se feina amb seguretat.

Un avís important: { ...bici } és una còpia superficial. Copia el primer nivell de propietats; si hi hagués objectes imbricats, continuarien compartits. El nostre domini és pla, així que no ens afecta, però tingues-ho present.

Per què React ho fa així

Podria semblar una complicació gratuïta, però comparar referències és una operació instantània, mentre que comparar en profunditat dos objectes grans a cada render seria caríssim. La immutabilitat és el preu que es paga per unes comprovacions de canvi barates, i de propina fa que les dades siguin més fàcils de raonar i de depurar.

  1. Què ha de ser estat i què es pot derivar

Aquest apartat t'estalviarà més errors que cap altre.

Si un valor es pot calcular a partir de les props o d'un altre estat, NO el guardis a l'estat. Calcula'l durant el render.

Un valor calculat en el render està sempre sincronitzat, perquè es recalcula cada vegada. Un valor duplicat a l'estat cal recordar d'actualitzar-lo, i el dia que se n'oblidi un, la pantalla mentirà.

// ❌ MALAMENT: estat duplicat
function ResumFlota({ flota }) {
  const [total, setTotal] = useState(flota.length);
  const [disponibles, setDisponibles] = useState(
    flota.filter(b => b.estat === 'disponible').length
  );
  // Cada canvi a 'flota' obliga a recordar actualitzar DOS estats més.
  // El dia que se n'oblidi un, el resum menteix.
}
// ✅ BÉ: valors derivats, calculats a cada render
function ResumFlota({ flota }) {
  const total = flota.length;
  const disponibles = flota.filter(b => b.estat === 'disponible').length;
  const noDisponibles = total - disponibles;
  // Impossible que es desincronitzin: es recalculen a cada render.
}

Una llista de comprovació per decidir:

Pregunta Si la resposta és sí…
Arriba per props? No és estat; és una prop
Es pot calcular a partir de props o un altre estat? No és estat; és un valor derivat
Es manté igual durant tota la vida del component? No és estat; és una constant fora del component
Canvia amb el temps, el canvia aquest component i no es pot deduir de res més? És estat

La preocupació habitual —«no serà car recalcular a cada render?»— gairebé sempre és infundada: un filter sobre unes desenes o centenars d'elements és menyspreable. I quan de veritat hi hagi un càlcul costós, hi ha una eina específica per memoïtzar-lo, useMemo, que s'estudia a 08-03. Optimitzar abans de mesurar és, com vas veure a 01-05, la manera més eficaç de complicar el codi sense guanyar-hi res.

  1. CicloUrbano: SelectorTipus i el comptador de disponibles

Afegirem dues peces amb estat al catàleg.

SelectorTipus: recordar el tipus triat

// src/components/SelectorTipus.jsx
import { useState } from 'react';

const TIPUS = ['todos', 'urbana', 'electrica', 'carga'];

/**
 * Selector del tipus de bicicleta.
 * De moment guarda el tipus triat en el seu propi estat i només el mostra.
 * A 04-01 l'estat pujarà al pare per poder filtrar la llista de veritat.
 */
function SelectorTipus() {
  const [tipusTriat, setTipusTriat] = useState('todos');

  return (
    <div className="selector-tipus">
      <p className="selector-tipus__titol">Filtra per tipus:</p>

      {TIPUS.map(tipus => (
        <button
          key={tipus}
          className={
            tipus === tipusTriat
              ? 'selector-tipus__boto selector-tipus__boto--actiu'
              : 'selector-tipus__boto'
          }
          onClick={() => setTipusTriat(tipus)}
        >
          {tipus}
        </button>
      ))}

      <p className="selector-tipus__seleccio">
        Selecció actual: <strong>{tipusTriat}</strong>
      </p>
    </div>
  );
}

export default SelectorTipus;

Punt per punt:

  • const TIPUS = [...] fora del component. És una constant que mai no canvia. Declarar-la fora evita recrear l'array a cada render, i a més deixa clar que no és estat.
  • useState('todos'): l'estat arrenca en «todos», el valor que mostra tota la flota.
  • onClick={() => setTipusTriat(tipus)}: la funció fletxa permet passar el tipus de cada botó. Sense ella, setTipusTriat(tipus) s'executaria durant el render.
  • La classe condicional ressalta el botó actiu comparant tipus === tipusTriat. És un valor derivat: no cal un estat botoActiu a part.
  • {TIPUS.map(...)} genera els quatre botons; el key és obligatori a les llistes, com vas veure a 01-05. La tècnica completa és el contingut de Llistes i Claus; aquí s'usa sobre una constant fixa per no repetir quatre vegades el mateix botó.

Prem els botons: la selecció canvia, el botó actiu es ressalta i el text de sota s'actualitza. El component té memòria.

Encara no filtra res, i és a propòsit: l'estat viu dins de SelectorTipus i LlistaBicicletes no el pot veure, perquè l'estat és privat. Perquè el filtre funcioni de veritat, l'estat haurà de pujar al pare comú. Aquest és exactament el problema que resol Elevar l'Estat.

ResumFlota: valors derivats, zero estat

A la lliçó 02-01 vas escriure ResumFlota amb els números «5, 3, 2» a mà. Ara es calculen:

// src/components/ResumFlota.jsx

/**
 * Resum numèric de la flota de CicloUrbano.
 * Props:
 *  - flota (array de bicicletes, obligatori)
 *
 * No té estat: tots els seus números són valors DERIVATS de la prop.
 */
function ResumFlota({ flota }) {
  const total = flota.length;
  const disponibles = flota.filter(b => b.estat === 'disponible').length;
  const llogades = flota.filter(b => b.estat === 'alquilada').length;
  const enManteniment = flota.filter(b => b.estat === 'mantenimiento').length;

  return (
    <section className="resum-flota">
      <h2>Resum de la flota</h2>
      <p>Total de bicicletes: {total}</p>
      <p>Disponibles: {disponibles}</p>
      <p>Llogades: {llogades}</p>
      <p>En manteniment: {enManteniment}</p>
    </section>
  );
}

export default ResumFlota;

Amb les dades del domini.js: total 5, disponibles 3, llogades 1, en manteniment 1. I si demà entra una bicicleta nova a l'array, el resum s'actualitza sol. Un component sense estat no és un component pobre: és un component que no es pot desincronitzar.

App munta les peces

// src/App.jsx
import { bicicletas } from './dades/domini.js';
import Capcalera from './components/Capcalera.jsx';
import ResumFlota from './components/ResumFlota.jsx';
import SelectorTipus from './components/SelectorTipus.jsx';
import LlistaBicicletes from './components/LlistaBicicletes.jsx';
import PeuDePagina from './components/PeuDePagina.jsx';

function App() {
  return (
    <>
      <Capcalera />
      <main>
        <ResumFlota flota={bicicletas} />
        <SelectorTipus />
        <LlistaBicicletes
          primera={bicicletas[0]}
          segona={bicicletas[1]}
          tercera={bicicletas[2]}
        />
      </main>
      <PeuDePagina />
    </>
  );
}

export default App;

I els estils de les peces noves:

/* Afegir al final de src/index.css */
.resum-flota,
.selector-tipus,
.comptador-places {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
  max-width: 32rem;
}

.selector-tipus__titol {
  margin: 0 0 0.5rem;
  font-weight: 600;
}

.selector-tipus__boto {
  border: 1px solid #d9e2ec;
  background: #f5f7fa;
  color: #1f2933;
  border-radius: 999px;
  padding: 0.35rem 0.9rem;
  margin-right: 0.5rem;
  cursor: pointer;
  font: inherit;
}

.selector-tipus__boto--actiu {
  background: #12805c;
  border-color: #12805c;
  color: #ffffff;
}

.selector-tipus__seleccio {
  margin: 0.75rem 0 0;
  font-size: 0.9rem;
}

Obre React DevTools, selecciona SelectorTipus i veuràs el seu estat al panell dret, canviant en directe a cada clic. És la millor eina per entendre què està passant.

Errors Habituals i Consells

  • Modificar l'estat directament. lliures = 5 o flota.push(nova) no disparen cap render. Sempre a través de la funció actualitzadora, i sempre amb còpies.
  • Esperar el valor nou just després d'actualitzar. setLliures(11); console.log(lliures); imprimeix el valor vell. El valor nou arriba al render següent.
  • Encadenar actualitzacions amb la forma directa. Tres setLliures(lliures + 1) seguits sumen un, no tres. Si el valor depèn de l'anterior, fes servir setLliures(prev => prev + 1).
  • Retornar un objecte sense parèntesis en la forma funcional. prev => { ...prev, x: 1 } no retorna res. Cal escriure prev => ({ ...prev, x: 1 }).
  • Guardar a l'estat alguna cosa que es pot calcular. Duplicar dades garanteix que algun dia es desincronitzin. Deriva-ho en el render.
  • Posar a l'estat alguna cosa que arriba per props sense motiu. useState(props.bicicleta) congela el valor inicial: si el pare passa una altra bicicleta, l'estat no se n'assabenta. Només es fa en el cas molt concret d'un valor inicial que després el component controla, i convé anomenar la prop bicicletaInicial perquè quedi clar.
  • Declarar useState dins d'un if. Trenca l'ordre dels hooks i produeix errors desconcertants. Sempre al nivell superior.
  • Consell: comença amb l'estat mínim. És més fàcil afegir un estat que descobrir que en tens quatre que es contradiuen.
  • Consell: usa la pestanya Components de DevTools. Veure l'estat real d'un component en cada moment resol la majoria de dubtes més ràpid que qualsevol console.log.

Exercicis

Exercici 1

Crea PanellReserva a src/components/PanellReserva.jsx. Ha de rebre per props una bicicleta i gestionar en el seu propi estat el nombre d'hores de la reserva, amb valor inicial 1. Mostra el model de la bicicleta, les hores seleccionades i el preu total (hores × preuHora), amb dos botons: un que suma una hora i un altre que en resta una, sense baixar mai d'1.

Fes servir la forma funcional en les dues actualitzacions i decideix amb criteri si el preu total ha de ser estat o valor derivat.

Exercici 2

Aquest component pretén marcar una bicicleta com a «llogada» en prémer el botó, però la pantalla no canvia mai. Explica per què amb precisió i corregeix-lo.

import { useState } from 'react';
import { bicicletas } from '../dades/domini.js';

function GestorFlota() {
  const [flota, setFlota] = useState(bicicletas);

  function llogarPrimera() {
    flota[0].estat = 'alquilada';
    setFlota(flota);
  }

  return (
    <section>
      <p>Estat de {flota[0].model}: {flota[0].estat}</p>
      <button onClick={llogarPrimera}>Llogar</button>
    </section>
  );
}

Exercici 3

Per a cadascuna d'aquestes dades de CicloUrbano, decideix si ha de ser estat, prop, valor derivat o constant fora del component, i justifica-ho en una frase.

  1. L'array bicicletas importat de domini.js i usat per App.
  2. El text que l'usuari escriu al cercador del catàleg.
  3. El nombre de bicicletes que compleixen el filtre actual.
  4. La llista de tipus vàlids: ['todos', 'urbana', 'electrica', 'carga'].
  5. La bicicleta concreta que rep una TargetaBicicleta.
  6. Si el panell de detall d'una estació està desplegat.
  7. El preu total d'una reserva, calculat a partir de les hores i del preu per hora.
  8. El nom de l'estació mostrat en una targeta.

Solucions

Solució 1.

// src/components/PanellReserva.jsx
import { useState } from 'react';

/**
 * Panell de reserva d'una bicicleta.
 * Props:
 *  - bicicleta (objecte, obligatori) { id, model, tipus, estat, estacioId, preuHora }
 */
function PanellReserva({ bicicleta }) {
  const [hores, setHores] = useState(1);

  // Valor DERIVAT: no ha de ser estat, es recalcula a cada render
  const preuTotal = (hores * bicicleta.preuHora).toFixed(2).replace('.', ',');

  function afegirHora() {
    setHores(anterior => anterior + 1);
  }

  function treureHora() {
    setHores(anterior => (anterior > 1 ? anterior - 1 : 1));
  }

  return (
    <section className="panell-reserva">
      <h3>Reservar {bicicleta.model}</h3>
      <p>Hores: {hores}</p>
      <p>
        Total: <strong>{preuTotal} €</strong>
      </p>
      <button onClick={treureHora}>−1 hora</button>
      <button onClick={afegirHora}>+1 hora</button>
    </section>
  );
}

export default PanellReserva;

Les decisions clau:

  • hores és estat: canvia amb la interacció i no es pot deduir de res més.
  • preuTotal és derivat: guardar-lo a l'estat obligaria a actualitzar-lo als dos gestors, i n'hi hauria prou d'oblidar-se'n en un perquè el total mentís.
  • Forma funcional als dos gestors: el valor nou depèn de l'anterior, així que és l'opció correcta per definició.
  • El límit inferior va dins de l'actualització, aplicat sobre anterior, no sobre la variable del render. Així continua sent correcte encara que hi hagi diverses actualitzacions encuades.

Solució 2.

Per què falla. Hi ha dos problemes encadenats:

  1. Mutació de l'estat. flota[0].estat = 'alquilada' modifica l'objecte dins de l'array de l'estat. Encara pitjor: useState(bicicletas) guarda la referència a l'array importat de domini.js, així que la mutació contamina el mòdul sencer per a tota l'aplicació.
  2. Mateixa referència. setFlota(flota) li passa a React exactament el mateix array que ja tenia. React compara referències, conclou que no ha canviat res i no dispara cap render. La dada en memòria sí que ha canviat; la pantalla no se n'assabenta.

Versió corregida:

import { useState } from 'react';
import { bicicletas } from '../dades/domini.js';

function GestorFlota() {
  const [flota, setFlota] = useState(bicicletas);

  function llogarPrimera() {
    setFlota(anterior =>
      anterior.map((bici, index) =>
        index === 0 ? { ...bici, estat: 'alquilada' } : bici
      )
    );
  }

  return (
    <section>
      <p>
        Estat de {flota[0].model}: {flota[0].estat}
      </p>
      <button onClick={llogarPrimera}>Llogar</button>
    </section>
  );
}

export default GestorFlota;

map retorna un array nou (referència nova → hi ha render) i l'objecte de la posició 0 se substitueix per una còpia modificada ({ ...bici, estat: 'alquilada' }), deixant intactes l'original i la resta d'elements.

Una millora addicional: identificar la bicicleta pel seu id en lloc de per l'índex, bici.id === 'bici-001', és més robust davant de canvis d'ordre.

Solució 3.

Dada Classificació Justificació
1. Array bicicletas de domini.js Constant importada És una dada fixa del mòdul; App no la modifica. Quan vingui d'un servidor serà estat, però això és el Mòdul 7
2. Text del cercador Estat Canvia amb cada pulsació de l'usuari i no es pot deduir de res
3. Nombre de bicicletes que compleixen el filtre Valor derivat Es calcula amb un filter sobre la flota i el filtre actual. Duplicar-lo a l'estat garanteix desincronització
4. Llista de tipus vàlids Constant fora del component Mai no canvia; declarar-la dins la recrearia a cada render sense cap motiu
5. La bicicleta d'una TargetaBicicleta Prop Ve del pare i la targeta només la llegeix
6. Si el detall està desplegat Estat És interacció pura de l'usuari, típicament un booleà local del component
7. Preu total d'una reserva Valor derivat hores × preuHora; sempre es pot recalcular
8. Nom de l'estació en una targeta Prop El pare sap a quina estació pertany; la targeta la rep i la mostra

Conclusió

L'estat és la memòria d'un component i el segon disparador del render. Ara saps per què una variable normal no serveix —React no la vigila i a més es reinicia a cada crida a la funció— i com es declara l'alternativa: const [valor, setValor] = useState(inicial), sempre al nivell superior del component. Saps que la variable de lectura és fixa durant tot un render i que el valor nou arriba al següent, que React agrupa les actualitzacions i que no renderitza si el valor no canvia.

Tens també les tres regles que eviten la majoria d'errors amb l'estat:

  1. Forma funcional (prev => …) sempre que el valor nou depengui de l'anterior.
  2. Immutabilitat: copiar objectes amb { ...obj } i arrays amb map, filter o [...arr], mai mutar, perquè React compara referències.
  3. No guardar el que es pot calcular: els valors derivats es recalculen en el render i així no es poden desincronitzar.

A CicloUrbano has afegit un SelectorTipus que recorda el tipus triat i un ResumFlota que, deliberadament, no té estat perquè tots els seus números es deriven de la flota. I has descobert el límit d'aquesta lliçó: l'estat és privat i local, de manera que SelectorTipus no pot filtrar LlistaBicicletes perquè ningú més veu el seu estat. Resoldre això és l'objectiu d'Elevar l'Estat, i la mecànica completa de useState t'espera a 05-01.

Ens queda una peça per tancar el mòdul. Els teus components ja encapsulen marcatge i lògica, però l'estil continua dispers en un index.css global que creix sense control i en el qual dues classes amb el mateix nom acabaran xocant tard o d'hora. A l'última lliçó, Estils en els Components: CSS, Mòduls i Utilitats, veuràs les quatre estratègies per donar estil a un component en React —CSS global, estils en línia, CSS Modules i utilitats— aplicades totes a TargetaBicicleta per poder-les comparar, i adoptarem la que CicloUrbano usarà a partir d'aquí.

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