La lección anterior terminó con una pregunta sin responder: si las props vienen de fuera y son de solo lectura, ¿cómo cambia algo en una aplicación React? El catálogo de CicloUrbano ya muestra bicicletas de verdad, pero es una fotografía: no reacciona a nada. Cuando el usuario elija un tipo de bicicleta, reserve una unidad o filtre por estación, algo tiene que cambiar dentro de la aplicación y provocar un nuevo dibujado. Ese algo es el estado: la memoria propia de un componente, el único dato que un componente puede modificar y el disparador del ciclo de renderizado que estudiaste en la lección 01-05. En esta lección verás por qué una variable de JavaScript corriente no sirve, cómo se declara estado con useState, por qué el estado está aislado por instancia, cómo actualizarlo sin perder actualizaciones, por qué hay que copiar objetos y arrays en lugar de modificarlos y —quizá lo más importante— qué merece ser estado y qué no.

Contenido

  1. Qué es el estado
  2. Estado frente a props
  3. Por qué una variable normal no funciona
  4. useState: declarar, leer y actualizar
  5. Actualizar el estado dispara un render
  6. El estado es local y está aislado por instancia
  7. Actualizaciones basadas en el valor anterior
  8. Inmutabilidad: objetos y arrays en el estado
  9. Qué debe ser estado y qué se puede derivar
  10. CicloUrbano: SelectorTipo y el contador de disponibles

  1. Qué es el estado

El estado (state) es un conjunto de datos que un componente guarda entre renders y que, al cambiar, hace que React vuelva a renderizarlo.

Fíjate en las dos mitades de la definición, porque las dos son imprescindibles:

  • Se conserva entre renders. Una variable normal declarada dentro de un componente nace y muere en cada render. El estado sobrevive.
  • Al cambiar, provoca un render. No es solo almacenamiento: es almacenamiento conectado a la pantalla.

En la lección 01-05 viste que hay dos cosas que disparan un render: el render inicial y un cambio de estado. Ahora ya sabes manejar el segundo.

Ejemplos de estado en CicloUrbano:

Dato ¿Por qué es estado?
El tipo de bicicleta seleccionado en el filtro El usuario lo cambia y la lista debe reaccionar
El número de horas de una reserva en curso Cambia al pulsar los botones del formulario
Si el panel de detalles está abierto o cerrado Cambia con la interacción
El texto escrito en el buscador Cambia con cada tecla

Y ejemplos de lo que no es estado:

Dato Por qué no
El array bicicletas de dominio.js Es un dato fijo importado; no lo cambia el componente
El nombre de la estación que recibe la tarjeta Viene por props: pertenece al padre
El número de bicicletas disponibles Se calcula a partir de los datos; ver apartado 9

  1. Estado frente a props

Las dos son fuentes de datos para un componente, y confundirlas es el error conceptual más frecuente al empezar.

Props Estado
Origen El componente padre El propio componente
Quién puede cambiarlo Solo el padre Solo el propio componente
¿Se puede modificar desde dentro? No, son de solo lectura Sí, con su función actualizadora
¿Sobrevive entre renders? Llegan de nuevo en cada render Sí, React lo conserva
Efecto de cambiarlo El padre re-renderiza y el hijo recibe valores nuevos El componente se re-renderiza
Visibilidad El padre las conoce Privado: nadie más lo ve
Analogía Los argumentos de una función Una variable que la función recuerda entre llamadas
En CicloUrbano bicicleta, estado, nombreEstacion El tipo filtrado, las horas de reserva

Una regla práctica que resuelve casi todas las dudas:

¿Este dato lo cambia el propio componente? Si sí, es estado. Si viene de fuera y él solo lo lee, es una prop.

Y un matiz que llegará en el Módulo 4: cuando dos componentes hermanos necesitan el mismo dato cambiante, el estado sube al padre común y baja a los hijos como props. Ese patrón se llama elevar el estado y se estudia en Elevando el Estado. En esta lección todo el estado se queda dentro de un solo componente.

  1. Por qué una variable normal no funciona

Vamos a intentarlo de la forma «obvia» y a ver exactamente dónde falla. Este componente muestra las plazas libres de la estación Plaza Mayor y debería descontar una al pulsar el botón.

// src/componentes/ContadorPlazas.jsx  — VERSIÓN ROTA, no la uses
function ContadorPlazas() {
  let libres = 12;                 // variable normal

  function ocuparPlaza() {
    libres = libres - 1;
    console.log('Ahora hay', libres, 'plazas libres');
  }

  return (
    <section className="contador-plazas">
      <p>Plaza Mayor · Plazas libres: {libres}</p>
      <button onClick={ocuparPlaza}>Ocupar una plaza</button>
    </section>
  );
}

export default ContadorPlazas;

Ejecútalo y pulsa el botón tres veces. La consola dice 11, 10, 9… y la pantalla sigue mostrando 12.

Hay dos fallos independientes, y entenderlos por separado es la clave de toda la lección:

Fallo 1: React no se entera

Modificar una variable de JavaScript es una operación privada de JavaScript. React no vigila tus variables: no hay nada que las conecte con la pantalla. Sin un cambio de estado, no hay disparo de render, y sin render la pantalla no se actualiza. Es exactamente lo que viste en 01-05: el render se dispara por el arranque o por un cambio de estado, y aquí no ha ocurrido ninguno de los dos.

Fallo 2: la variable se reinicia

Y aunque forzáramos un render por otro medio, tampoco funcionaría. Un componente es una función, y React la llama de nuevo en cada render. Cada llamada ejecuta let libres = 12; otra vez, desde cero. El valor anterior se ha perdido: era una variable local de una llamada que ya terminó.

flowchart TD
    A["Render 1: se ejecuta la función<br/>let libres = 12"] --> B["Pulsas el botón<br/>libres pasa a 11 en memoria"]
    B --> C{"¿React lo sabe?"}
    C -- No --> D["No hay render.<br/>La pantalla sigue en 12"]
    C -. "y si lo forzáramos" .-> E["Render 2: la función se ejecuta otra vez<br/>let libres = 12 -> vuelve a 12"]

Conclusión: un componente necesita algo que (a) React vigile para disparar el render y (b) sobreviva a la reejecución de la función. Ese algo es el estado, y se declara con useState.

  1. useState: declarar, leer y actualizar

useState es un hook: una función especial de React que permite a un componente de función acceder a capacidades del motor. El Módulo 5 estudia los hooks a fondo, y useState cubre este en todos sus detalles. Aquí nos quedamos con el uso básico, que es el 90 % de los casos.

import { useState } from 'react';

function ContadorPlazas() {
  const [libres, setLibres] = useState(12);
  // …
}

Esa única línea contiene cuatro cosas:

Parte Qué es
useState(12) Declara un trozo de estado con valor inicial 12
libres La variable de lectura: el valor actual del estado en este render
setLibres La función actualizadora: la única forma válida de cambiarlo
const [a, b] = … Desestructuración de array: useState devuelve un array de dos elementos

Sobre esa desestructuración: useState devuelve [valor, funcionActualizadora], y las llaves cuadradas extraen los dos elementos y les ponen nombre. Los nombres los eliges tú; la convención universal es algo y setAlgo, en ese orden.

Ahora la versión que funciona:

// src/componentes/ContadorPlazas.jsx  — VERSIÓN CORRECTA
import { useState } from 'react';

/**
 * Contador de plazas libres de una estación de CicloUrbano.
 * Props:
 *  - nombreEstacion (cadena, opcional, por defecto 'Plaza Mayor')
 *  - plazasIniciales (número, opcional, por defecto 12)
 */
function ContadorPlazas({ nombreEstacion = 'Plaza Mayor', plazasIniciales = 12 }) {
  const [libres, setLibres] = useState(plazasIniciales);

  function ocuparPlaza() {
    setLibres(libres - 1);
  }

  return (
    <section className="contador-plazas">
      <p>
        {nombreEstacion} · Plazas libres: {libres}
      </p>
      <button onClick={ocuparPlaza}>Ocupar una plaza</button>
    </section>
  );
}

export default ContadorPlazas;

Los cambios respecto a la versión rota son solo tres: importar useState, declarar el estado y llamar a setLibres en lugar de asignar. El resto del componente es idéntico. Pulsa el botón: ahora la pantalla baja a 11, 10, 9…

Sobre onClick: aquí lo usamos como mínimo necesario para provocar un cambio. Los manejadores de eventos —su sintaxis completa, el objeto de evento, la propagación— son el contenido de Manejo de Eventos. Quédate de momento con la regla: se pasa la función sin paréntesis.

Dos reglas sobre dónde va useState

Los hooks tienen normas de uso estrictas. De momento, dos:

  1. Siempre en el nivel superior del componente. Nunca dentro de un if, un bucle, una función anidada o después de un return condicional.
  2. Solo dentro de componentes (o de hooks personalizados). No en una función auxiliar cualquiera.

El motivo —React identifica cada trozo de estado por el orden en que se declara— y el resto de las reglas se explican en Hooks: Introducción y Uso Básico.

Varios trozos de estado

Un componente puede declarar tantos como necesite, y es lo habitual:

function PanelReserva() {
  const [horas, setHoras] = useState(1);
  const [tipo, setTipo] = useState('urbana');
  const [confirmada, setConfirmada] = useState(false);
  // …
}

Mantenerlos separados es preferible a meterlos en un único objeto: cada dato se actualiza por su cuenta y el código queda mucho más claro. Cuando varios valores cambian siempre juntos y con reglas complejas, existe una herramienta específica, useReducer, que se ve en 05-05.

  1. Actualizar el estado dispara un render

Esto conecta directamente con la lección 01-05. Cuando llamas a setLibres(11), ocurre esta secuencia:

flowchart TD
    A["Pulsas el botón"] --> B["Se ejecuta setLibres(11)"]
    B --> C["React marca el componente<br/>como pendiente de renderizar"]
    C --> D["React vuelve a llamar a<br/>ContadorPlazas()"]
    D --> E["Esta vez useState devuelve 11"]
    E --> F["Se produce un árbol de elementos nuevo"]
    F --> G["Reconciliación: React compara<br/>con el árbol anterior"]
    G --> H["Commit: solo cambia el texto<br/>del párrafo en el DOM"]

Tres consecuencias que hay que interiorizar:

a) El valor no cambia en la línea siguiente. La variable libres de este render es una constante: vale lo que valía cuando React ejecutó la función. El valor nuevo aparece en el render siguiente.

function ocuparPlaza() {
  console.log(libres);       // 12
  setLibres(libres - 1);
  console.log(libres);       // ¡sigue siendo 12!
}

Esto no es un error ni un retraso: es coherencia. Durante todo un render, todos los valores permanecen fijos, lo cual evita una clase entera de errores en los que unas partes de la pantalla van adelantadas respecto a otras.

b) React agrupa las actualizaciones. Si llamas varias veces a funciones actualizadoras dentro del mismo manejador, React no renderiza una vez por llamada: las agrupa y hace un solo render al final. Es lo que se conoce como batching.

c) Si el valor no cambia, no hay render. React compara el valor nuevo con el actual. Si son iguales (con la comparación de Object.is), se ahorra el trabajo.

setLibres(12);   // si libres ya vale 12, React no renderiza

Ojo con esto último cuando el estado es un objeto: si mutas el objeto y le pasas la misma referencia, React concluye que no ha cambiado nada y no renderiza. Es la causa de la mitad de los «no se actualiza la pantalla» que verás en tu vida, y el motivo del apartado 8.

  1. El estado es local y está aislado por instancia

Cada vez que usas un componente en el JSX creas una instancia distinta, y cada instancia tiene su propio estado, completamente independiente.

// src/App.jsx (fragmento)
<ContadorPlazas nombreEstacion="Plaza Mayor" plazasIniciales={20} />
<ContadorPlazas nombreEstacion="Parque Norte" plazasIniciales={15} />
<ContadorPlazas nombreEstacion="Estación Central" plazasIniciales={30} />

Tres contadores en pantalla. Pulsa el botón del primero: baja de 20 a 19 y los otros dos no se inmutan. Comparten el código, no los datos.

flowchart TD
    APP[App] --> C1["ContadorPlazas<br/>Plaza Mayor<br/>estado: libres = 19"]
    APP --> C2["ContadorPlazas<br/>Parque Norte<br/>estado: libres = 15"]
    APP --> C3["ContadorPlazas<br/>Estación Central<br/>estado: libres = 30"]

Esta propiedad tiene consecuencias importantes:

  • El estado es privado. Ningún otro componente puede leerlo ni escribirlo. Ni el padre. Esa encapsulación es la que hace que un componente sea razonable en aislamiento.
  • Si dos componentes necesitan compartirlo, el estado está en el sitio equivocado. Hay que subirlo al padre común (04-01).
  • El estado va ligado a la posición en el árbol. Si React destruye el componente —porque cambia de tipo o desaparece de la pantalla—, su estado se pierde. Es exactamente la heurística de reconciliación que estudiaste en 01-05, vista ahora desde el otro lado.

  1. Actualizaciones basadas en el valor anterior

La función actualizadora admite dos formas de uso, y la diferencia importa.

setLibres(libres - 1);          // Forma directa: pasas el valor nuevo
setLibres(anterior => anterior - 1);   // Forma funcional: pasas una función

En la forma funcional, React llama a tu función pasándole el valor más reciente del estado y usa lo que devuelves como valor nuevo.

Por qué la forma directa puede fallar

Imagina un botón que libera tres plazas de golpe:

function liberarTresPlazas() {
  setLibres(libres + 1);
  setLibres(libres + 1);
  setLibres(libres + 1);
}

Si libres vale 12, esperarías 15. Obtienes 13. La razón: libres es una constante fija durante todo este render, con valor 12. Las tres llamadas dicen literalmente «pon el estado a 13», tres veces. Y como React agrupa las actualizaciones, el resultado final es 13.

Con la forma funcional:

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

React encola las tres funciones y las aplica en orden, cada una sobre el resultado de la anterior. Ahora sí: 15.

Forma Sintaxis Cuándo usarla
Directa setLibres(5) El valor nuevo no depende del anterior
Funcional setLibres(prev => prev - 1) El valor nuevo depende del anterior

Regla práctica: si en el argumento aparece la variable de estado, usa la forma funcional. Es siempre correcta, no cuesta nada y evita una familia entera de errores, incluidos los que solo aparecen con código asíncrono.

  1. Inmutabilidad: objetos y arrays en el estado

Aquí está la trampa más importante del estado en React.

Nunca modifiques directamente un objeto o un array que esté en el estado. Crea una copia nueva.

El motivo es el que viste en el apartado 5: React decide si hay que renderizar comparando referencias. Si mutas el objeto, la referencia no cambia, React concluye que nada ha cambiado y no renderiza.

El caso de un objeto

Supongamos que un componente guarda en su estado la bicicleta seleccionada y queremos marcarla como reservada.

const [bicicletaSeleccionada, setBicicletaSeleccionada] = useState(bicicletas[0]);
// ❌ MAL: mutación. La pantalla no se actualiza.
function reservar() {
  bicicletaSeleccionada.estado = 'alquilada';
  setBicicletaSeleccionada(bicicletaSeleccionada);   // misma referencia -> sin render
}
// ✅ BIEN: copia con el campo cambiado
function reservar() {
  setBicicletaSeleccionada({
    ...bicicletaSeleccionada,
    estado: 'alquilada'
  });
}

El operador de propagación copia todas las propiedades del objeto original en uno nuevo, y estado: 'alquilada', al ir después, sobrescribe esa clave. El resultado es un objeto distinto, con una referencia nueva, y React renderiza.

Y con la forma funcional, que es la recomendada:

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

Fíjate en los paréntesis que envuelven las llaves: anterior => ({ … }). Sin ellos, JavaScript interpreta las llaves como el cuerpo de la función flecha y devuelve undefined. Es una errata clásica.

El caso de un array

Un componente que guarda una lista de bicicletas en su estado:

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

Los métodos de array se dividen en dos grupos, y esta tabla merece que la tengas a mano:

Operación ❌ Muta (prohibido) ✅ Devuelve copia (correcto)
Añadir al final push [...flota, nueva]
Añadir al principio unshift [nueva, ...flota]
Eliminar splice, pop, shift flota.filter(b => b.id !== id)
Reemplazar un elemento flota[0] = … flota.map(b => b.id === id ? nueva : b)
Ordenar sort [...flota].sort(…)
Invertir reverse [...flota].reverse()

Marcar una bicicleta como reservada dentro de un array combina las dos ideas: hay que copiar el array y copiar el objeto que cambia.

// ✅ Copia del array (map) + copia del objeto modificado (spread)
function reservarBicicleta(id) {
  setFlota(anterior =>
    anterior.map(bici =>
      bici.id === id
        ? { ...bici, estado: 'alquilada' }   // objeto nuevo solo para esta
        : bici                                // las demás, tal cual
    )
  );
}

// Al llamar a reservarBicicleta('bici-001'):
// - flota es un array NUEVO (map siempre crea uno)
// - bici-001 es un objeto NUEVO con estado 'alquilada'
// - bici-002 ... bici-005 son EXACTAMENTE los mismos objetos de antes

Ese último detalle es elegante y deliberado: los elementos que no cambian conservan su referencia, lo que permite a React —y a las optimizaciones del Módulo 8— saltarse trabajo con seguridad.

Un aviso importante: { ...bici } es una copia superficial. Copia el primer nivel de propiedades; si hubiera objetos anidados, seguirían compartidos. Nuestro dominio es plano, así que no nos afecta, pero tenlo presente.

Por qué React lo hace así

Podría parecer una complicación gratuita, pero comparar referencias es una operación instantánea, mientras que comparar en profundidad dos objetos grandes en cada render sería carísimo. La inmutabilidad es el precio que se paga por unas comprobaciones de cambio baratas, y de propina hace que los datos sean más fáciles de razonar y de depurar.

  1. Qué debe ser estado y qué se puede derivar

Este apartado te ahorrará más errores que ningún otro.

Si un valor se puede calcular a partir de las props o de otro estado, NO lo guardes en el estado. Calcúlalo durante el render.

Un valor calculado en el render está siempre sincronizado, porque se recalcula cada vez. Un valor duplicado en el estado hay que acordarse de actualizarlo, y el día que se olvide uno, la pantalla mentirá.

// ❌ MAL: estado duplicado
function ResumenFlota({ flota }) {
  const [total, setTotal] = useState(flota.length);
  const [disponibles, setDisponibles] = useState(
    flota.filter(b => b.estado === 'disponible').length
  );
  // Cada cambio en 'flota' obliga a recordar actualizar DOS estados más.
  // El día que se olvide uno, el resumen miente.
}
// ✅ BIEN: valores derivados, calculados en cada render
function ResumenFlota({ flota }) {
  const total = flota.length;
  const disponibles = flota.filter(b => b.estado === 'disponible').length;
  const noDisponibles = total - disponibles;
  // Imposible que se desincronicen: se recalculan en cada render.
}

Una lista de comprobación para decidir:

Pregunta Si la respuesta es sí…
¿Llega por props? No es estado; es una prop
¿Se puede calcular de props u otro estado? No es estado; es un valor derivado
¿Se mantiene igual durante toda la vida del componente? No es estado; es una constante fuera del componente
¿Cambia con el tiempo, lo cambia este componente y no se puede deducir de nada más? Es estado

La preocupación habitual —«¿no será caro recalcular en cada render?»— casi siempre es infundada: un filter sobre unas decenas o cientos de elementos es despreciable. Y cuando de verdad haya un cálculo costoso, existe una herramienta específica para memorizarlo, useMemo, que se estudia en 08-03. Optimizar antes de medir es, como viste en 01-05, la forma más eficaz de complicar el código sin ganar nada.

  1. CicloUrbano: SelectorTipo y el contador de disponibles

Vamos a añadir dos piezas con estado al catálogo.

SelectorTipo: recordar el tipo elegido

// src/componentes/SelectorTipo.jsx
import { useState } from 'react';

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

/**
 * Selector del tipo de bicicleta.
 * De momento guarda el tipo elegido en su propio estado y solo lo muestra.
 * En 04-01 el estado subirá al padre para poder filtrar la lista de verdad.
 */
function SelectorTipo() {
  const [tipoElegido, setTipoElegido] = useState('todos');

  return (
    <div className="selector-tipo">
      <p className="selector-tipo__titulo">Filtrar por tipo:</p>

      {TIPOS.map(tipo => (
        <button
          key={tipo}
          className={
            tipo === tipoElegido
              ? 'selector-tipo__boton selector-tipo__boton--activo'
              : 'selector-tipo__boton'
          }
          onClick={() => setTipoElegido(tipo)}
        >
          {tipo}
        </button>
      ))}

      <p className="selector-tipo__seleccion">
        Selección actual: <strong>{tipoElegido}</strong>
      </p>
    </div>
  );
}

export default SelectorTipo;

Punto por punto:

  • const TIPOS = [...] fuera del componente. Es una constante que nunca cambia. Declararla fuera evita recrear el array en cada render, y además deja claro que no es estado.
  • useState('todos'): el estado arranca en «todos», el valor que muestra toda la flota.
  • onClick={() => setTipoElegido(tipo)}: la función flecha permite pasar el tipo de cada botón. Sin ella, setTipoElegido(tipo) se ejecutaría durante el render.
  • La clase condicional resalta el botón activo comparando tipo === tipoElegido. Es un valor derivado: no hace falta un estado botonActivo aparte.
  • {TIPOS.map(...)} genera los cuatro botones; el key es obligatorio en las listas, como viste en 01-05. La técnica completa es el contenido de Listas y Claves; aquí se usa sobre una constante fija para no repetir cuatro veces el mismo botón.

Pulsa los botones: la selección cambia, el botón activo se resalta y el texto de abajo se actualiza. El componente tiene memoria.

Aún no filtra nada, y es a propósito: el estado vive dentro de SelectorTipo y ListaBicicletas no puede verlo, porque el estado es privado. Para que el filtro funcione de verdad, el estado tendrá que subir al padre común. Ese es exactamente el problema que resuelve Elevando el Estado.

ResumenFlota: valores derivados, cero estado

En la lección 02-01 escribiste ResumenFlota con los números «5, 3, 2» a mano. Ahora se calculan:

// src/componentes/ResumenFlota.jsx

/**
 * Resumen numérico de la flota de CicloUrbano.
 * Props:
 *  - flota (array de bicicletas, obligatorio)
 *
 * No tiene estado: todos sus números son valores DERIVADOS de la prop.
 */
function ResumenFlota({ flota }) {
  const total = flota.length;
  const disponibles = flota.filter(b => b.estado === 'disponible').length;
  const alquiladas = flota.filter(b => b.estado === 'alquilada').length;
  const enMantenimiento = flota.filter(b => b.estado === 'mantenimiento').length;

  return (
    <section className="resumen-flota">
      <h2>Resumen de la flota</h2>
      <p>Total de bicicletas: {total}</p>
      <p>Disponibles: {disponibles}</p>
      <p>Alquiladas: {alquiladas}</p>
      <p>En mantenimiento: {enMantenimiento}</p>
    </section>
  );
}

export default ResumenFlota;

Con los datos del dominio.js: total 5, disponibles 3, alquiladas 1, en mantenimiento 1. Y si mañana entra una bicicleta nueva en el array, el resumen se actualiza solo. Un componente sin estado no es un componente pobre: es un componente que no puede desincronizarse.

App monta las piezas

// src/App.jsx
import { bicicletas } from './datos/dominio.js';
import Cabecera from './componentes/Cabecera.jsx';
import ResumenFlota from './componentes/ResumenFlota.jsx';
import SelectorTipo from './componentes/SelectorTipo.jsx';
import ListaBicicletas from './componentes/ListaBicicletas.jsx';
import PieDePagina from './componentes/PieDePagina.jsx';

function App() {
  return (
    <>
      <Cabecera />
      <main>
        <ResumenFlota flota={bicicletas} />
        <SelectorTipo />
        <ListaBicicletas
          primera={bicicletas[0]}
          segunda={bicicletas[1]}
          tercera={bicicletas[2]}
        />
      </main>
      <PieDePagina />
    </>
  );
}

export default App;

Y los estilos de las piezas nuevas:

/* Añadir al final de src/index.css */
.resumen-flota,
.selector-tipo,
.contador-plazas {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
  max-width: 32rem;
}

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

.selector-tipo__boton {
  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-tipo__boton--activo {
  background: #12805c;
  border-color: #12805c;
  color: #ffffff;
}

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

Abre React DevTools, selecciona SelectorTipo y verás su estado en el panel derecho, cambiando en directo con cada clic. Es la mejor herramienta para entender qué está pasando.

Errores Comunes y Consejos

  • Modificar el estado directamente. libres = 5 o flota.push(nueva) no disparan ningún render. Siempre a través de la función actualizadora, y siempre con copias.
  • Esperar el valor nuevo justo después de actualizar. setLibres(11); console.log(libres); imprime el valor viejo. El valor nuevo llega en el render siguiente.
  • Encadenar actualizaciones con la forma directa. Tres setLibres(libres + 1) seguidos suman uno, no tres. Si el valor depende del anterior, usa setLibres(prev => prev + 1).
  • Devolver un objeto sin paréntesis en la forma funcional. prev => { ...prev, x: 1 } no devuelve nada. Hay que escribir prev => ({ ...prev, x: 1 }).
  • Guardar en el estado algo que se puede calcular. Duplicar datos garantiza que algún día se desincronicen. Deriva en el render.
  • Poner en el estado algo que llega por props sin motivo. useState(props.bicicleta) congela el valor inicial: si el padre pasa otra bicicleta, el estado no se entera. Solo se hace en el caso muy concreto de un valor inicial que luego el componente controla, y conviene nombrar la prop bicicletaInicial para que quede claro.
  • Declarar useState dentro de un if. Rompe el orden de los hooks y produce errores desconcertantes. Siempre en el nivel superior.
  • Consejo: empieza con el estado mínimo. Es más fácil añadir un estado que descubrir que tienes cuatro que se contradicen.
  • Consejo: usa la pestaña Components de DevTools. Ver el estado real de un componente en cada momento resuelve la mayoría de las dudas más rápido que cualquier console.log.

Ejercicios

Ejercicio 1

Crea PanelReserva en src/componentes/PanelReserva.jsx. Debe recibir por props una bicicleta y gestionar en su propio estado el número de horas de la reserva, con valor inicial 1. Muestra el modelo de la bicicleta, las horas seleccionadas y el precio total (horas × precioHora), con dos botones: uno que suma una hora y otro que resta una, sin bajar nunca de 1.

Usa la forma funcional en las dos actualizaciones y decide con criterio si el precio total debe ser estado o valor derivado.

Ejercicio 2

Este componente pretende marcar una bicicleta como «alquilada» al pulsar el botón, pero la pantalla no cambia nunca. Explica por qué con precisión y corrígelo.

import { useState } from 'react';
import { bicicletas } from '../datos/dominio.js';

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

  function alquilarPrimera() {
    flota[0].estado = 'alquilada';
    setFlota(flota);
  }

  return (
    <section>
      <p>Estado de {flota[0].modelo}: {flota[0].estado}</p>
      <button onClick={alquilarPrimera}>Alquilar</button>
    </section>
  );
}

Ejercicio 3

Para cada uno de estos datos de CicloUrbano, decide si debe ser estado, prop, valor derivado o constante fuera del componente, y justifícalo en una frase.

  1. El array bicicletas importado de dominio.js y usado por App.
  2. El texto que el usuario escribe en el buscador del catálogo.
  3. El número de bicicletas que cumplen el filtro actual.
  4. La lista de tipos válidos: ['todos', 'urbana', 'electrica', 'carga'].
  5. La bicicleta concreta que recibe una TarjetaBicicleta.
  6. Si el panel de detalle de una estación está desplegado.
  7. El precio total de una reserva, calculado a partir de las horas y del precio por hora.
  8. El nombre de la estación mostrado en una tarjeta.

Soluciones

Solución 1.

// src/componentes/PanelReserva.jsx
import { useState } from 'react';

/**
 * Panel de reserva de una bicicleta.
 * Props:
 *  - bicicleta (objeto, obligatorio) { id, modelo, tipo, estado, estacionId, precioHora }
 */
function PanelReserva({ bicicleta }) {
  const [horas, setHoras] = useState(1);

  // Valor DERIVADO: no debe ser estado, se recalcula en cada render
  const precioTotal = (horas * bicicleta.precioHora).toFixed(2).replace('.', ',');

  function anadirHora() {
    setHoras(anterior => anterior + 1);
  }

  function quitarHora() {
    setHoras(anterior => (anterior > 1 ? anterior - 1 : 1));
  }

  return (
    <section className="panel-reserva">
      <h3>Reservar {bicicleta.modelo}</h3>
      <p>Horas: {horas}</p>
      <p>
        Total: <strong>{precioTotal} €</strong>
      </p>
      <button onClick={quitarHora}>−1 hora</button>
      <button onClick={anadirHora}>+1 hora</button>
    </section>
  );
}

export default PanelReserva;

Las decisiones clave:

  • horas es estado: cambia con la interacción y no se puede deducir de nada más.
  • precioTotal es derivado: guardarlo en el estado obligaría a actualizarlo en los dos manejadores, y bastaría con olvidarse en uno para que el total mintiera.
  • Forma funcional en los dos manejadores: el valor nuevo depende del anterior, así que es la opción correcta por definición.
  • El límite inferior va dentro de la actualización, aplicado sobre anterior, no sobre la variable del render. Así sigue siendo correcto aunque haya varias actualizaciones encoladas.

Solución 2.

Por qué falla. Hay dos problemas encadenados:

  1. Mutación del estado. flota[0].estado = 'alquilada' modifica el objeto dentro del array del estado. Peor aún: useState(bicicletas) guarda la referencia al array importado de dominio.js, así que la mutación contamina el módulo entero para toda la aplicación.
  2. Misma referencia. setFlota(flota) le pasa a React exactamente el mismo array que ya tenía. React compara referencias, concluye que no ha cambiado nada y no dispara ningún render. El dato en memoria sí ha cambiado; la pantalla no se entera.

Versión corregida:

import { useState } from 'react';
import { bicicletas } from '../datos/dominio.js';

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

  function alquilarPrimera() {
    setFlota(anterior =>
      anterior.map((bici, indice) =>
        indice === 0 ? { ...bici, estado: 'alquilada' } : bici
      )
    );
  }

  return (
    <section>
      <p>
        Estado de {flota[0].modelo}: {flota[0].estado}
      </p>
      <button onClick={alquilarPrimera}>Alquilar</button>
    </section>
  );
}

export default GestorFlota;

map devuelve un array nuevo (referencia nueva → hay render) y el objeto de la posición 0 se sustituye por una copia modificada ({ ...bici, estado: 'alquilada' }), dejando intactos el original y los demás elementos.

Una mejora adicional: identificar la bicicleta por su id en lugar de por su índice, bici.id === 'bici-001', es más robusto ante cambios de orden.

Solución 3.

Dato Clasificación Justificación
1. Array bicicletas de dominio.js Constante importada Es un dato fijo del módulo; App no lo modifica. Cuando venga de un servidor será estado, pero eso es el Módulo 7
2. Texto del buscador Estado Cambia con cada pulsación del usuario y no se puede deducir de nada
3. Número de bicicletas que cumplen el filtro Valor derivado Se calcula con un filter sobre la flota y el filtro actual. Duplicarlo en el estado garantiza desincronización
4. Lista de tipos válidos Constante fuera del componente Nunca cambia; declararla dentro la recrearía en cada render sin ningún motivo
5. La bicicleta de una TarjetaBicicleta Prop Viene del padre y la tarjeta solo la lee
6. Si el detalle está desplegado Estado Es interacción pura del usuario, típicamente un booleano local del componente
7. Precio total de una reserva Valor derivado horas × precioHora; siempre se puede recalcular
8. Nombre de la estación en una tarjeta Prop El padre sabe a qué estación pertenece; la tarjeta la recibe y la muestra

Conclusión

El estado es la memoria de un componente y el segundo disparador del render. Ahora sabes por qué una variable normal no sirve —React no la vigila y además se reinicia en cada llamada a la función— y cómo se declara la alternativa: const [valor, setValor] = useState(inicial), siempre en el nivel superior del componente. Sabes que la variable de lectura es fija durante todo un render y que el valor nuevo llega en el siguiente, que React agrupa las actualizaciones y que no renderiza si el valor no cambia.

Tienes también las tres reglas que evitan la mayoría de los errores con estado:

  1. Forma funcional (prev => …) siempre que el valor nuevo dependa del anterior.
  2. Inmutabilidad: copiar objetos con { ...obj } y arrays con map, filter o [...arr], nunca mutar, porque React compara referencias.
  3. No guardar lo que se puede calcular: los valores derivados se recalculan en el render y así no pueden desincronizarse.

En CicloUrbano has añadido un SelectorTipo que recuerda el tipo elegido y un ResumenFlota que, deliberadamente, no tiene estado porque todos sus números se derivan de la flota. Y has descubierto el límite de esta lección: el estado es privado y local, de modo que SelectorTipo no puede filtrar ListaBicicletas porque nadie más ve su estado. Resolver eso es el objetivo de Elevando el Estado, y la mecánica completa de useState te espera en 05-01.

Nos queda una pieza para cerrar el módulo. Tus componentes ya encapsulan marcado y lógica, pero el estilo sigue disperso en un index.css global que crece sin control y en el que dos clases con el mismo nombre acabarán chocando tarde o temprano. En la última lección, Estilos en los Componentes: CSS, Módulos y Utilidades, verás las cuatro estrategias para dar estilo a un componente en React —CSS global, estilos en línea, CSS Modules y utilidades— aplicadas todas a TarjetaBicicleta para poder compararlas, y adoptaremos la que CicloUrbano usará a partir de ahí.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados