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
- Las tres fases de la vida de un componente
- Los métodos de montaje:
constructoryrender componentDidMount: cuando el componente ya está en pantallacomponentDidUpdate: la comparación con props y estado previoscomponentWillUnmounty las fugas de memoriashouldComponentUpdatey los métodosUNSAFE_- Ejemplo completo:
PanelActividadcon temporizador - Tabla de equivalencias: clase → función con hooks
- Por qué la equivalencia es solo aproximada
- Convivir con clases en una base de código real
- 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
componentDidMountotra 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.
- Los métodos de montaje:
constructor y render
constructor y renderimport { 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: inicializarthis.statey enlazar métodos conbind.super(props)es obligatorio; sin él,this.propsestaría vacío dentro del constructor y JavaScript lanzaría un error al usarthis.render()debe ser puro: mirathis.propsythis.state, devuelve JSX y no hace nada más. Nada de peticiones al servidor, nada desetState, nada de tocar el DOM directamente. React puede llamarlo varias veces antes de reflejar nada en pantalla, así que cualquier efecto secundario dentro derenderse 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.
componentDidMount: cuando el componente ya está en pantalla
componentDidMount: cuando el componente ya está en pantallaSe 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.
componentDidUpdate: la comparación con props y estado previos
componentDidUpdate: la comparación con props y estado previosSe 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.
componentWillUnmount y las fugas de memoria
componentWillUnmount y las fugas de memoriaSe 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:
- El componente se monta y arranca un intervalo que se ejecuta cada segundo.
- La persona usuaria navega a otra pantalla y el componente se desmonta.
- El intervalo sigue vivo:
setIntervales del navegador, no de React, y a React nunca se le informó de que hay que pararlo. - Cada segundo, el callback intenta hacer
setStatesobre 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. - 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.
shouldComponentUpdate y los métodos UNSAFE_
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.
- Ejemplo completo:
PanelActividad con temporizador
PanelActividad con temporizadorEste 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 elbinddeactualizarReloj, porque se pasará asetIntervaly allí perdería suthis(02-02).componentDidMount: arranca el intervalo y guarda su identificador enthis.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áuseRefen 05-03.componentDidUpdate: la comparaciónpropsPrevias.reserva.id !== this.props.reserva.ides imprescindible. Sin ella, elsetStatese ejecutaría en cada actualización —y hay una por segundo— provocando el bucle del apartado 4.componentWillUnmount: elclearIntervalque 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,restantesy 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.
- 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.
- 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 |
- Convivir con clases en una base de código real
Las clases no van a desaparecer, y hay tres razones para ello:
- React mantiene su compatibilidad. No hay ningún anuncio de eliminación: el código con clases seguirá funcionando.
- 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.
- 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
bindpor todas partes, con la misma lógica duplicada entrecomponentDidMountycomponentDidUpdate, 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
renderes el error que se avisó en 02-02.
Errores Comunes y Consejos
setStatesin comparación dentro decomponentDidUpdate. Bucle infinito y «Maximum update depth exceeded». Compara siemprepropsPreviasconthis.propsantes de actualizar.- Olvidar
componentWillUnmount. Temporizadores, escuchadores y suscripciones que sobreviven al componente: fuga de memoria y avisos desetStatesobre componentes desmontados. Por cada suscripción al montar, una baja al desmontar. removeEventListenercon 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 usarthisen el constructor. Siempre la primera línea. - Mutar el estado directamente.
this.state.horas = 5no dispara ningún render y deja la pantalla desactualizada. Solothis.setState. - Confiar en que
this.stateestá actualizado justo después desetState. No lo está: las actualizaciones se agrupan. Usa la forma funcionalthis.setState((previo) => …)cuando el valor nuevo dependa del anterior, exactamente igual que conuseState. - Guardar en el estado datos que no se pintan. El identificador de un temporizador va en
this, no enthis.state; guardarlo como estado provoca renders inútiles. - Consejo: usa
console.logcon 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. ConStrictModeverá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.
- Registrar en un servicio de analítica que se ha visitado la ficha de una bicicleta, cada vez que cambia la bicicleta mostrada.
- Abrir una conexión en tiempo real que informa de las plazas libres de la estación seleccionada.
- Calcular el precio total de una reserva a partir de
precioHorayhoras.
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
componentWillUnmountni se guarda el identificador. Cada montaje deja un temporizador vivo para siempre. componentDidUpdatellama asetStatesin 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 propestacionId.
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
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
