Un componente no existe siempre. Aparece en pantalla, se actualiza mientras sus props o su estado cambian, y desaparece cuando ya no hace falta. Esas tres fases —montaje, actualización y desmontaje— existen en React desde el primer día, y durante años se gestionaron con un conjunto de métodos especiales que solo tienen los componentes de clase. Hoy no escribirás ninguno de esos métodos en código nuevo, pero los encontrarás sin falta en cualquier proyecto con unos años encima, y los tutoriales antiguos siguen explicando React en esos términos. En esta lección aprenderás a leer el ciclo de vida clásico, a entender qué problema resolvía cada método, a traducirlo a la sintaxis moderna con una tabla de equivalencias, y —lo más importante— por qué el modelo mental correcto en React 19 ya no es «momentos del ciclo de vida» sino sincronización con un sistema externo.

Contenido

  1. Las tres fases de la vida de un componente
  2. Los métodos de montaje: constructor y render
  3. componentDidMount: cuando el componente ya está en pantalla
  4. componentDidUpdate: la comparación con props y estado previos
  5. componentWillUnmount y las fugas de memoria
  6. shouldComponentUpdate y los métodos UNSAFE_
  7. Ejemplo completo: PanelActividad con temporizador
  8. Tabla de equivalencias: clase → función con hooks
  9. Por qué la equivalencia es solo aproximada
  10. Convivir con clases en una base de código real

  1. Las tres fases de la vida de un componente

Todo componente montado en el DOM atraviesa este recorrido:

flowchart TD
    A["MONTAJE<br/>El componente entra en el árbol"] --> A1["constructor()"]
    A1 --> A2["render()"]
    A2 --> A3["React inserta el DOM"]
    A3 --> A4["componentDidMount()"]
    A4 --> B{"¿Cambian props<br/>o estado?"}
    B -- "sí" --> B1["shouldComponentUpdate()<br/>(opcional)"]
    B1 --> B2["render()"]
    B2 --> B3["React actualiza el DOM"]
    B3 --> B4["componentDidUpdate()"]
    B4 --> B
    B -- "el componente sale del árbol" --> C["componentWillUnmount()"]
    C --> D["DESMONTAJE<br/>React retira el DOM y olvida el estado"]

Tres ideas antes de entrar en el detalle:

  • El montaje ocurre una sola vez por instancia. Si el componente se desmonta y vuelve a montarse, es una instancia nueva: estado a cero y componentDidMount otra vez. Es exactamente lo que viste en 01-05, cuando un cambio de tipo de elemento destruye el subárbol y pierde su estado.
  • La actualización ocurre muchas veces, una por cada cambio de props o de estado.
  • El desmontaje ocurre una sola vez, y es la última oportunidad de limpiar lo que el componente haya dejado abierto fuera de React.

Y una advertencia sobre StrictMode, que ya conoces de 01-03: en desarrollo React monta, desmonta y vuelve a montar cada componente a propósito. Verás componentDidMount dos veces y componentWillUnmount una entre medias. No es un fallo: es una comprobación de que tu limpieza funciona. En producción ocurre una sola vez.

  1. Los métodos de montaje: constructor y render

import { Component } from 'react';

class ContadorAlquileres extends Component {
  constructor(props) {
    super(props);                      // OBLIGATORIO y siempre lo primero
    this.state = { alquileres: 0 };    // única asignación directa permitida
    this.manejarAlquiler = this.manejarAlquiler.bind(this);   // el bind de 02-02
  }

  manejarAlquiler() {
    this.setState((estadoPrevio) => ({ alquileres: estadoPrevio.alquileres + 1 }));
  }

  render() {
    return (
      <div>
        <p>Alquileres registrados: {this.state.alquileres}</p>
        <button type="button" onClick={this.manejarAlquiler}>Registrar alquiler</button>
      </div>
    );
  }
}
  • constructor(props) se ejecuta una vez, antes del primer render. Sirve para dos cosas y solo dos: inicializar this.state y enlazar métodos con bind. super(props) es obligatorio; sin él, this.props estaría vacío dentro del constructor y JavaScript lanzaría un error al usar this.
  • render() debe ser puro: mira this.props y this.state, devuelve JSX y no hace nada más. Nada de peticiones al servidor, nada de setState, nada de tocar el DOM directamente. React puede llamarlo varias veces antes de reflejar nada en pantalla, así que cualquier efecto secundario dentro de render se ejecutaría un número impredecible de veces.

Esa exigencia de pureza es la razón de ser de todo lo que viene después: si render no puede tener efectos secundarios, hacen falta otros sitios donde ponerlos. Los métodos del ciclo de vida son esos sitios.

  1. componentDidMount: cuando el componente ya está en pantalla

Se ejecuta una sola vez, justo después de que React haya insertado el componente en el DOM real. Es el lugar clásico para todo lo que necesita que el elemento exista.

componentDidMount() {
  // 1. Pedir datos al servidor
  fetch('/api/bicicletas')
    .then((respuesta) => respuesta.json())
    .then((bicicletas) => this.setState({ bicicletas, cargando: false }));

  // 2. Suscribirse a algo externo
  window.addEventListener('resize', this.manejarRedimension);

  // 3. Arrancar temporizadores
  this.temporizador = setInterval(this.actualizarReloj, 1000);

  // 4. Medir el DOM, que ya existe
  const alto = this.contenedor.current.offsetHeight;
}

Aquí sí está permitido llamar a setState: provoca un segundo render inmediato, antes de que el navegador pinte, así que la persona usuaria no ve un parpadeo. Es el patrón habitual de «cargando → datos listos».

Lo que no debe hacerse: llamar a setState incondicionalmente con un valor que se pueda calcular durante el render. Eso duplica el trabajo sin motivo.

  1. componentDidUpdate: la comparación con props y estado previos

Se ejecuta después de cada actualización, excepto la primera. Su firma es la parte que hay que entender:

componentDidUpdate(propsPrevias, estadoPrevio) {
  // this.props y this.state ya tienen los valores NUEVOS
  // propsPrevias y estadoPrevio tienen los ANTERIORES
}

React entrega los valores anteriores porque, sin ellos, sería imposible saber qué ha cambiado. Y esa comparación no es opcional: es obligatoria si dentro vas a llamar a setState.

// CORRECTO: la comparación evita el bucle infinito
componentDidUpdate(propsPrevias) {
  if (propsPrevias.estacionId !== this.props.estacionId) {
    this.cargarBicicletasDeEstacion(this.props.estacionId);
  }
}
// ROTO: bucle infinito garantizado
componentDidUpdate() {
  this.cargarBicicletasDeEstacion(this.props.estacionId);
  // carga -> setState -> actualización -> componentDidUpdate -> carga -> …
}

El ciclo es exactamente este: setState provoca una actualización, la actualización provoca componentDidUpdate, y componentDidUpdate vuelve a llamar a setState. El navegador se queda bloqueado y React acaba lanzando «Maximum update depth exceeded».

Este método es también el origen de una duplicación clásica en código antiguo: la carga inicial va en componentDidMount y la recarga al cambiar de estación va en componentDidUpdate, con la misma llamada escrita dos veces en dos métodos separados por cincuenta líneas. Basta con tocar una y olvidar la otra para que aparezca un fallo desconcertante. Ese defecto estructural es una de las razones por las que nacieron los hooks.

  1. componentWillUnmount y las fugas de memoria

Se ejecuta justo antes de que React retire el componente del DOM. Es la última oportunidad de deshacer lo que se hizo al montar.

componentWillUnmount() {
  clearInterval(this.temporizador);
  window.removeEventListener('resize', this.manejarRedimension);
  this.suscripcion.cancelar();
}

Qué pasa si se olvida, con el caso más ilustrativo, un setInterval:

  1. El componente se monta y arranca un intervalo que se ejecuta cada segundo.
  2. La persona usuaria navega a otra pantalla y el componente se desmonta.
  3. El intervalo sigue vivo: setInterval es del navegador, no de React, y a React nunca se le informó de que hay que pararlo.
  4. Cada segundo, el callback intenta hacer setState sobre un componente que ya no existe. En el mejor de los casos React lo ignora; en el peor, el callback mantiene vivas por referencia todas las variables del componente —incluidos sus datos— y el navegador no puede liberarlas.
  5. Si la persona entra y sale de esa pantalla veinte veces, hay veinte intervalos ejecutándose a la vez. La aplicación se ralentiza sin causa aparente.

Eso es una fuga de memoria, y es el fallo más común asociado al ciclo de vida. La regla es simétrica y sin excepciones:

Si al montar haces… Al desmontar debes…
setInterval / setTimeout clearInterval / clearTimeout
addEventListener removeEventListener con la misma referencia de función
Abrir un WebSocket o un EventSource Cerrarlo
Suscribirte a un servicio Cancelar la suscripción
Lanzar una petición cuya respuesta actualiza estado Abortarla o marcar el componente como desmontado

El matiz de removeEventListener merece un aviso: solo elimina el manejador si le pasas exactamente la misma función que registraste. Con window.addEventListener('resize', () => this.algo()) y window.removeEventListener('resize', () => this.algo()) estás registrando una función y eliminando otra distinta, aunque el código sea idéntico. Por eso los manejadores se guardan en this o se enlazan en el constructor.

  1. shouldComponentUpdate y los métodos UNSAFE_

shouldComponentUpdate(propsSiguientes, estadoSiguiente) devuelve un booleano: true (por defecto) para renderizar, false para saltarse el render.

shouldComponentUpdate(propsSiguientes) {
  // Solo re-renderiza si cambia la bicicleta o su estado
  return (
    propsSiguientes.bicicleta.id !== this.props.bicicleta.id ||
    propsSiguientes.bicicleta.estado !== this.props.bicicleta.estado
  );
}

Es una herramienta de rendimiento, no de corrección, y peligrosa si se usa mal: una comparación incompleta hace que el componente ignore cambios reales y muestre datos desactualizados, con un fallo muy difícil de diagnosticar. La regla es la misma que se aplicará en el Módulo 8: no lo uses hasta haber medido un problema real.

Existió también PureComponent, una clase base que implementa shouldComponentUpdate haciendo una comparación superficial de todas las props y el estado. Su equivalente moderno es React.memo (08-02).

Los métodos obsoletos con prefijo UNSAFE_

Tres métodos antiguos fueron marcados como inseguros en React 16.3 y renombrados con el prefijo UNSAFE_:

Método Qué hacía Por qué se desaconseja
UNSAFE_componentWillMount Se ejecutaba antes del primer render No hay DOM todavía; se usaba mal para pedir datos y no funciona con renderizado en servidor
UNSAFE_componentWillReceiveProps Avisaba de props nuevas antes de renderizar Invitaba a copiar props en el estado, el error de 04-01
UNSAFE_componentWillUpdate Se ejecutaba antes de aplicar los cambios No puede leer el DOM ni llamar a setState con seguridad

El motivo común: no son seguros con el renderizado interrumpible. Desde React 18, React puede empezar a renderizar, pausar y descartar el trabajo. Un método que se ejecuta antes del render puede llegar a ejecutarse varias veces o para un render que se abandona. Los métodos que se ejecutan después (componentDidMount, componentDidUpdate) no tienen ese problema, porque para entonces el resultado ya está confirmado.

Si los ves en código real: se pueden usar todavía, con su prefijo, pero son señal clara de código sin migrar.

  1. Ejemplo completo: PanelActividad con temporizador

Este es un componente de CicloUrbano escrito como clase, tal y como podría estar en una versión antigua del proyecto. Muestra la hora actual y cuánto tiempo lleva activa la reserva res-01.

// src/componentes/PanelActividad.jsx  — CÓDIGO HEREDADO (clase)
import { Component } from 'react';
import estilos from './PanelActividad.module.css';

/**
 * Panel de actividad de una reserva en curso de CicloUrbano.
 * Props:
 *  - reserva (objeto Reserva, obligatorio)
 */
class PanelActividad extends Component {
  constructor(props) {
    super(props);
    this.state = {
      ahora: new Date(),
      registros: 0
    };
    this.actualizarReloj = this.actualizarReloj.bind(this);
  }

  // MONTAJE: el componente ya está en pantalla, arrancamos el temporizador
  componentDidMount() {
    console.log('[PanelActividad] montado, arrancando temporizador');
    this.temporizador = setInterval(this.actualizarReloj, 1000);
  }

  // ACTUALIZACIÓN: solo reaccionamos si cambia la reserva mostrada
  componentDidUpdate(propsPrevias) {
    if (propsPrevias.reserva.id !== this.props.reserva.id) {
      console.log('[PanelActividad] la reserva ha cambiado, reiniciando contador');
      this.setState({ registros: 0 });
    }
  }

  // DESMONTAJE: damos de baja el temporizador. SIN ESTO HAY FUGA
  componentWillUnmount() {
    console.log('[PanelActividad] desmontado, parando temporizador');
    clearInterval(this.temporizador);
  }

  actualizarReloj() {
    this.setState((estadoPrevio) => ({
      ahora: new Date(),
      registros: estadoPrevio.registros + 1
    }));
  }

  render() {
    const { reserva } = this.props;
    const inicio = new Date(reserva.fechaInicio);
    const minutosTranscurridos = Math.max(
      0,
      Math.floor((this.state.ahora - inicio) / 60000)
    );
    const minutosContratados = reserva.horas * 60;
    const restantes = minutosContratados - minutosTranscurridos;

    return (
      <section className={estilos.panel}>
        <h2>Actividad de la reserva {reserva.id}</h2>
        <p>Hora actual: {this.state.ahora.toLocaleTimeString('es-ES')}</p>
        <p>Minutos transcurridos: {minutosTranscurridos}</p>
        <p className={restantes < 15 ? estilos.urgente : undefined}>
          {restantes > 0
            ? `Quedan ${restantes} minutos de reserva.`
            : 'La reserva ha finalizado.'}
        </p>
      </section>
    );
  }
}

export default PanelActividad;

Recorrido por las piezas:

  • constructor: inicializa dos datos de estado y hace el bind de actualizarReloj, porque se pasará a setInterval y allí perdería su this (02-02).
  • componentDidMount: arranca el intervalo y guarda su identificador en this.temporizador. Se guarda en la instancia, no en el estado, porque no afecta a lo que se pinta y cambiarlo no debe provocar un render. Es el mismo razonamiento que justificará useRef en 05-03.
  • componentDidUpdate: la comparación propsPrevias.reserva.id !== this.props.reserva.id es imprescindible. Sin ella, el setState se ejecutaría en cada actualización —y hay una por segundo— provocando el bucle del apartado 4.
  • componentWillUnmount: el clearInterval que evita la fuga. Prueba a comentarlo, monta y desmonta el panel varias veces y mira la consola: los mensajes del reloj se acumulan, uno por cada instancia muerta.
  • render: puro. minutosTranscurridos, restantes y la clase condicional son valores derivados calculados en cada render, no estado. La misma distinción de 02-04, ahora en sintaxis de clase.

Ese mismo componente, escrito con hooks, ocupa la mitad:

// src/componentes/PanelActividad.jsx  — VERSIÓN MODERNA (adelanto de 05-02)
import { useState, useEffect } from 'react';
import estilos from './PanelActividad.module.css';

function PanelActividad({ reserva }) {
  const [ahora, setAhora] = useState(new Date());

  useEffect(() => {
    const temporizador = setInterval(() => setAhora(new Date()), 1000);
    return () => clearInterval(temporizador);   // la limpieza, JUNTO a lo que limpia
  }, []);

  const inicio = new Date(reserva.fechaInicio);
  const minutosTranscurridos = Math.max(0, Math.floor((ahora - inicio) / 60000));
  const restantes = reserva.horas * 60 - minutosTranscurridos;

  return (
    <section className={estilos.panel}>
      <h2>Actividad de la reserva {reserva.id}</h2>
      <p>Hora actual: {ahora.toLocaleTimeString('es-ES')}</p>
      <p>Minutos transcurridos: {minutosTranscurridos}</p>
      <p className={restantes < 15 ? estilos.urgente : undefined}>
        {restantes > 0 ? `Quedan ${restantes} minutos de reserva.` : 'La reserva ha finalizado.'}
      </p>
    </section>
  );
}

export default PanelActividad;

Observa la diferencia que más importa: el clearInterval está tres líneas debajo del setInterval, dentro del mismo bloque, en lugar de a cincuenta líneas de distancia en otro método. Olvidarlo se vuelve mucho más difícil. La API completa de useEffect —qué es esa función que se devuelve, qué significa el array vacío del final— se explica en 05-02; aquí solo interesa la comparación estructural.

  1. Tabla de equivalencias: clase → función con hooks

Método de clase Equivalente con hooks Lección
constructor + this.state = {…} useState(valorInicial) 05-01
this.setState({…}) La función actualizadora de useState 05-01
render() El cuerpo de la función, con su return 02-02
componentDidMount useEffect(() => { … }, []) 05-02
componentDidUpdate useEffect(() => { … }, [dependencias]) 05-02
componentWillUnmount La función de limpieza que devuelve useEffect 05-02
shouldComponentUpdate React.memo alrededor del componente 08-02
PureComponent React.memo 08-02
this.temporizador = … (dato no visual) useRef 05-03
static contextType useContext 05-04
this.miNodo = createRef() useRef 05-03
HOC o render props (04-02) Hook personalizado 05-06
getDerivedStateFromError / componentDidCatch Sin equivalente: siguen exigiendo clase 04-05
UNSAFE_componentWillReceiveProps Casi siempre: nada. Recalcula durante el render 04-01

La última fila merece énfasis. UNSAFE_componentWillReceiveProps se usaba sobre todo para copiar props en el estado cuando cambiaban, y esa necesidad casi nunca es real: si un valor depende de las props, se calcula durante el render como valor derivado, tal y como hiciste con bicicletasVisibles en 04-01. La ausencia de equivalente no es una carencia de los hooks: es que el problema estaba mal planteado.

  1. Por qué la equivalencia es solo aproximada

La tabla anterior es una ayuda para traducir, no una descripción de cómo funciona React moderno. Si te quedas con la idea de que «useEffect con array vacío es componentDidMount», escribirás efectos incorrectos. Tres diferencias reales:

Primera: useEffect no se ejecuta en los mismos instantes. Se ejecuta después de que el navegador haya pintado, mientras que componentDidMount lo hace antes. Para la mayoría de los casos da igual, pero si necesitas medir el DOM y ajustar el diseño sin parpadeo, hay un hook específico (useLayoutEffect, mencionado en 05-02).

Segunda: un componente puede tener muchos efectos, y una clase solo tenía un componentDidMount. En una clase, la suscripción al temporizador, la carga de datos y el registro de analítica compartían método aunque no tuvieran nada que ver. Con hooks, cada preocupación va en su propio useEffect, agrupada con su limpieza correspondiente. Se organiza por asunto, no por momento.

Tercera, y la que de verdad importa: el modelo mental cambia. La pregunta correcta ya no es «¿en qué momento del ciclo de vida hago esto?» sino:

«¿Qué sistema externo debe mantenerse sincronizado con el estado de este componente, y cómo se desincroniza cuando el componente deja de necesitarlo?»

Un efecto no es «código que se ejecuta al montar». Es una descripción de una sincronización: mientras este componente exista con estas props y este estado, debe haber un temporizador activo / una suscripción abierta / una conexión establecida; y cuando eso deje de ser cierto —porque el componente se desmonta o porque cambian las dependencias— la sincronización se deshace y, si procede, se rehace con los valores nuevos.

Ese cambio de mentalidad explica un comportamiento que desconcierta a quien viene de las clases: un efecto con dependencias puede ejecutar su limpieza y volver a montarse muchas veces durante la vida del componente, no solo al principio y al final. Con el modelo de «momentos del ciclo de vida» eso parece un error; con el modelo de sincronización es exactamente lo que debe pasar. Se desarrolla a fondo en useEffect.

Modelo mental antiguo (clases) Modelo mental correcto (React moderno)
«¿Cuándo se ejecuta esto?» «¿Con qué debe estar sincronizado esto?»
El código se agrupa por momento El código se agrupa por asunto
Montar y desmontar son eventos únicos Sincronizar y desincronizar pueden repetirse
La limpieza está lejos de lo que limpia La limpieza está junto a lo que limpia
Un método por fase Tantos efectos como sistemas externos

  1. Convivir con clases en una base de código real

Las clases no van a desaparecer, y hay tres razones para ello:

  1. React mantiene su compatibilidad. No hay ningún anuncio de eliminación: el código con clases seguirá funcionando.
  2. Migrar cuesta dinero y no añade funcionalidad. Un componente de clase que lleva cinco años funcionando sin fallos no es una prioridad para nadie.
  3. Los límites de error todavía exigen clase. Es la única pieza de React 19 que no tiene equivalente con hooks, y la verás en la próxima lección.

Cómo trabajar con ellas sin sufrir:

  • Puedes mezclarlas libremente. Un componente de función puede renderizar uno de clase y al revés, sin adaptadores. Ambos reciben props igual y participan en el mismo árbol.
  • No conviertas por conversión. Migra un componente de clase cuando ya tengas que tocarlo por otro motivo: un fallo, una funcionalidad nueva, un refactor pedido. La migración oportunista tiene el mejor equilibrio entre riesgo y beneficio.
  • Prioriza por dolor real. Los candidatos claros son los componentes con bind por todas partes, con la misma lógica duplicada entre componentDidMount y componentDidUpdate, o envueltos en varios HOC (04-02). Ahí la migración se paga sola.
  • Un componente de clase no puede usar hooks. Es una limitación absoluta: si necesitas un hook dentro de una clase, no hay atajo, hay que convertir el componente.
  • Al migrar, empieza por el inventario. Enumera qué métodos del ciclo de vida usa el componente, traduce cada uno con la tabla del apartado 8 y revisa después la lista de dependencias de cada efecto. Copiar solo el cuerpo de render es el error que se avisó en 02-02.

Errores Comunes y Consejos

  • setState sin comparación dentro de componentDidUpdate. Bucle infinito y «Maximum update depth exceeded». Compara siempre propsPrevias con this.props antes de actualizar.
  • Olvidar componentWillUnmount. Temporizadores, escuchadores y suscripciones que sobreviven al componente: fuga de memoria y avisos de setState sobre componentes desmontados. Por cada suscripción al montar, una baja al desmontar.
  • removeEventListener con otra función. Pasar una flecha nueva no elimina nada. Guarda la referencia en la instancia o enlaza en el constructor.
  • Olvidar super(props). Error inmediato al usar this en el constructor. Siempre la primera línea.
  • Mutar el estado directamente. this.state.horas = 5 no dispara ningún render y deja la pantalla desactualizada. Solo this.setState.
  • Confiar en que this.state está actualizado justo después de setState. No lo está: las actualizaciones se agrupan. Usa la forma funcional this.setState((previo) => …) cuando el valor nuevo dependa del anterior, exactamente igual que con useState.
  • Guardar en el estado datos que no se pintan. El identificador de un temporizador va en this, no en this.state; guardarlo como estado provoca renders inútiles.
  • Consejo: usa console.log con prefijo para aprender el orden. Pon un mensaje en cada método, monta y desmonta el componente, y observa la secuencia real en la consola. Con StrictMode verás el doble montaje de desarrollo, que es una buena forma de comprobar que tu limpieza funciona.
  • Consejo: no aprendas React moderno en términos de ciclo de vida. Usa la tabla para traducir código antiguo, pero razona los efectos nuevos en términos de sincronización.

Ejercicios

Ejercicio 1. Este componente de clase de CicloUrbano tiene tres errores relacionados con el ciclo de vida. Encuéntralos, explica qué provoca cada uno y corrígelo.

class ContadorEstaciones extends Component {
  constructor(props) {
    this.state = { visitas: 0, ahora: new Date() };
  }

  componentDidMount() {
    setInterval(() => this.setState({ ahora: new Date() }), 1000);
  }

  componentDidUpdate() {
    this.setState({ visitas: this.state.visitas + 1 });
  }

  render() {
    return <p>Visitas: {this.state.visitas} — {this.state.ahora.toLocaleTimeString('es-ES')}</p>;
  }
}

Ejercicio 2. Traduce a componente de función con hooks el siguiente RelojEstacion, que muestra la hora de una estación y se resuscribe cuando cambia la estación. No hace falta que domines useEffect: guíate por la tabla del apartado 8 y por el ejemplo del apartado 7.

class RelojEstacion extends Component {
  constructor(props) {
    super(props);
    this.state = { hora: new Date() };
    this.actualizar = this.actualizar.bind(this);
  }

  componentDidMount() {
    this.temporizador = setInterval(this.actualizar, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.temporizador);
  }

  actualizar() {
    this.setState({ hora: new Date() });
  }

  render() {
    return (
      <p>
        {this.props.estacion.nombre}: {this.state.hora.toLocaleTimeString('es-ES')}
      </p>
    );
  }
}

Ejercicio 3. Explica, para cada uno de estos tres casos de CicloUrbano, en qué método del ciclo de vida iría el código en una clase y con qué hook se resolvería hoy. Justifica la respuesta en términos de sincronización, no de momentos.

  1. Registrar en un servicio de analítica que se ha visitado la ficha de una bicicleta, cada vez que cambia la bicicleta mostrada.
  2. Abrir una conexión en tiempo real que informa de las plazas libres de la estación seleccionada.
  3. Calcular el precio total de una reserva a partir de precioHora y horas.

Soluciones

Solución 1. Los tres errores:

  • Falta super(props) en el constructor. JavaScript lanza «Must call super constructor before accessing 'this'» y el componente no llega a montarse.
  • El intervalo nunca se cancela: no hay componentWillUnmount ni se guarda el identificador. Cada montaje deja un temporizador vivo para siempre.
  • componentDidUpdate llama a setState sin comparar: cada actualización provoca otra, en bucle infinito. Y como el intervalo actualiza cada segundo, el bucle arrancaría solo aunque nadie tocase nada. Para contar visitas de verdad hace falta un dato con el que comparar; en la corrección suponemos que el componente recibe una prop estacionId.
class ContadorEstaciones extends Component {
  constructor(props) {
    super(props);                                     // 1. corregido
    this.state = { visitas: 0, ahora: new Date() };
  }

  componentDidMount() {
    this.temporizador = setInterval(                  // 2. guardamos la referencia
      () => this.setState({ ahora: new Date() }),
      1000
    );
  }

  componentDidUpdate(propsPrevias) {
    if (propsPrevias.estacionId !== this.props.estacionId) {   // 3. comparación
      this.setState((previo) => ({ visitas: previo.visitas + 1 }));
    }
  }

  componentWillUnmount() {
    clearInterval(this.temporizador);                 // 2. baja del temporizador
  }

  render() {
    return <p>Visitas: {this.state.visitas} — {this.state.ahora.toLocaleTimeString('es-ES')}</p>;
  }
}

Fíjate además en la forma funcional (previo) => ({ visitas: previo.visitas + 1 }): es la misma precaución que con useState, porque el valor nuevo depende del anterior y las actualizaciones se agrupan.

Solución 2.

// src/componentes/RelojEstacion.jsx
import { useState, useEffect } from 'react';

/**
 * Props:
 *  - estacion (objeto Estacion, obligatorio)
 */
function RelojEstacion({ estacion }) {
  const [hora, setHora] = useState(new Date());

  useEffect(() => {
    const temporizador = setInterval(() => setHora(new Date()), 1000);
    return () => clearInterval(temporizador);
  }, []);

  return (
    <p>
      {estacion.nombre}: {hora.toLocaleTimeString('es-ES')}
    </p>
  );
}

export default RelojEstacion;

Traducción método a método: this.state = { hora }useState(new Date()); componentDidMount → el cuerpo del useEffect con array vacío; componentWillUnmount → la función devuelta por el efecto; render → el return de la función. Desaparecen el constructor, el bind, los cuatro this y la clase entera: de 24 líneas a 12, y la limpieza queda pegada a lo que limpia.

Solución 3.

Caso Método de clase Solución moderna Razonamiento de sincronización
1. Registrar la visita a una ficha componentDidMount + componentDidUpdate comparando bicicleta.id useEffect con [bicicleta.id] en las dependencias Hay un sistema externo (la analítica) que debe enterarse cada vez que cambia la bicicleta mostrada. La duplicación entre dos métodos desaparece: un solo bloque cubre el primer envío y todos los siguientes
2. Conexión en tiempo real de plazas libres componentDidMount para abrir, componentDidUpdate para cerrar la anterior y abrir la nueva, componentWillUnmount para cerrar useEffect con [estacion.id] y una función de limpieza que cierra la conexión La conexión debe estar abierta mientras el componente muestre esa estación. Al cambiar de estación, la sincronización se deshace (cierra) y se rehace (abre) con el valor nuevo. Esto es exactamente lo que el modelo de «momentos» no sabe expresar
3. Calcular el precio total Ninguno Ningún hook: una variable calculada en el cuerpo de la función No hay sistema externo alguno. Es un valor derivado de props y estado, como bicicletasVisibles en 04-01. Meterlo en un efecto añadiría un render extra y una oportunidad de desincronización a cambio de nada

El tercer caso es el más instructivo: la mitad de los efectos innecesarios que se escriben en React son cálculos derivados disfrazados de sincronización.

Conclusión

Los componentes de React nacen, se actualizan y mueren, y esas tres fases se gestionaron durante años con los métodos del ciclo de vida de los componentes de clase: constructor y render para el montaje, componentDidMount para lo que necesita el DOM ya pintado, componentDidUpdate —con su comparación obligatoria de props previas— para reaccionar a los cambios, componentWillUnmount para dar de baja lo que se dio de alta y evitar fugas, shouldComponentUpdate para el rendimiento y los métodos UNSAFE_ como recordatorio de lo que no debe hacerse. Ahora sabes leerlos, corregir sus errores clásicos y traducirlos con la tabla de equivalencias.

Pero la conclusión importante es la del apartado 9: la equivalencia es aproximada y el modelo mental es distinto. En React moderno no se piensa en momentos del ciclo de vida, sino en sincronizar el componente con sistemas externos y deshacer esa sincronización cuando deja de hacer falta. Esa idea es la columna vertebral de useEffect.

Y queda una pregunta natural: si los hooks sustituyen a casi todo lo que hacían las clases, ¿qué son exactamente, por qué tienen reglas tan estrictas sobre dónde se pueden escribir y cuántos hay? La próxima lección es Hooks: Introducción y Uso Básico, donde verás el mecanismo interno que explica esas reglas, el catálogo completo de los hooks que usarás en el curso y tu primer componente de CicloUrbano que combina estado y efecto.

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