Actualizas la página y la pantalla se queda en blanco. O tal vez, al hacer clic en un botón, el ventilador de tu CPU se pone a toda marcha. Entonces, la consola escupe la temida advertencia: Maximum update depth exceeded. React ha frenado en seco porque tu componente está atrapado en un bucle infinito. Es uno de los errores más comunes en las aplicaciones de React, y suele derivar de un simple malentendido sobre cuándo se ejecuta realmente tu código.
Para solucionarlo, necesitas entender el momento exacto en que un renderizado se convierte en un re-renderizado, y por qué los cambios de estado deben mantenerse fuera de la ruta de renderizado.
Cómo React renderiza tu componente
React construye interfaces de usuario a partir de componentes. En el React moderno, esos componentes son funciones. Cada vez que React necesita mostrar tu componente en la pantalla, simplemente llama a esa función. Dentro de la función, puedes usar el state (estado) para recordar cosas entre renderizados. El estado le indica a React qué datos pertenecen al componente y, lo que es crucial, cuándo algo ha cambiado y la interfaz de usuario necesita actualizarse.
Cuando el estado cambia, React programa un nuevo renderizado. La función del componente se ejecuta de nuevo, devuelve un nuevo JSX y React actualiza el DOM para que coincida. En un uso normal, este ciclo es inofensivo. Haces clic en un botón, un manejador de eventos actualiza el estado, React vuelve a renderizar una vez y el usuario ve el nuevo texto o color.
El error aparece cuando un renderizado en sí mismo activa otra actualización de estado. Esa nueva actualización de estado activa otro renderizado, que a su vez activa otra actualización de estado. React tolera esto durante unas pocas docenas de ciclos, y luego lanza el error de profundidad máxima para proteger al navegador de un bloqueo total.
Un vistazo rápido al estado
Antes de diseccionar el bucle, recuerda cómo funciona el hook useState. Te ofrece exactamente dos cosas: una variable que contiene el valor actual y una función para cambiar ese valor.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
Aquí, message es 'Welcome' en el primer renderizado. Si más tarde llamas a setMessage('Goodbye'), React nota el cambio, llama a MessageComponent de nuevo y la interfaz ahora muestra "Goodbye". Todo funciona bien porque nada en el cuerpo del componente está llamando al setter automáticamente. El bucle comienza cuando el setter se dispara durante la fase de renderizado sin un evento externo.
Llamar a SetState directamente en el cuerpo
La forma más directa de crear un bucle infinito es llamar a una función de actualización de estado directamente dentro del cuerpo del componente. Debido a que el cuerpo del componente se ejecuta en cada renderizado, el setter se dispara en cada renderizado. Esa nueva actualización de estado provoca otro renderizado. El ciclo gira eternamente.
Así es como se ve el error:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
Cada vez que Counter se renderiza, incrementa count. React renderiza de nuevo para mostrar el nuevo número, ve setCount(count + 1) otra vez e incrementa una vez más. La solución es sencilla: nunca llames a un setter de estado en el nivel superior de tu componente durante el renderizado. Las actualizaciones de estado deben responder a eventos del usuario o efectos secundarios, no al acto de pintar la pantalla. Mueve esa actualización a un manejador de eventos en su lugar:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
La única excepción al llamar a los setters durante el renderizado es cuando estás calculando un nuevo estado a partir de las props, e incluso en ese caso, deberías usar un patrón diferente, como derivar el valor directamente o usar useEffect de forma intencionada.
Pasar una llamada a una función en lugar de una referencia
Otra causa frecuente es un error tipográfico sutil en JSX. Cuando adjuntas un manejador de eventos, necesitas pasar la función en sí. Si accidentalmente invocas la función allí mismo en el JSX, se ejecutará inmediatamente durante el ciclo de renderizado.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
Al escribir handleToggle() con paréntesis, no le estás dando a React una función para llamar más tarde cuando el usuario haga clic. La estás llamando ahora mismo mientras React está construyendo el DOM virtual. Como handleToggle actualiza el estado, el componente se vuelve a renderizar. Durante ese re-renderizado, React ve handleToggle() de nuevo y la llama otra vez. El bucle nunca termina. La versión correcta elimina los paréntesis:
return <button onClick={handleToggle}>Toggle</button>;
Si necesitas pasar argumentos, envuelve la llamada en una función anónima:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
Esta distinción confunde incluso a desarrolladores experimentados durante la refactorización. Presta atención a esos paréntesis.
La trampa de las dependencias de useEffect
Los efectos son el lugar adecuado para los efectos secundarios, como la obtención de datos, la sincronización con las APIs del navegador o la manipulación manual del DOM. Pero useEffect se ejecuta después de que React confirma el renderizado en la pantalla. Si tu efecto actualiza el estado, React volverá a renderizar. Normalmente eso está bien. Se convierte en un bucle cuando el efecto se ejecuta después de cada renderizado y siempre actualiza el mismo estado.
Consider this broken pattern:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Because there is no dependency array, this effect runs after every single render. It sets user, which triggers a render. After that render, the effect runs again and sets user again. React detects the spiral and throws the error.
The fix is to tell React when the effect actually needs to run by supplying a proper dependency array. If the effect should only run once on mount, pass an empty array:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
If the effect depends on a prop or a piece of state, include only that variable in the array. Be careful, though. Including a variable that changes every render will just recreate the same loop through a different door. For example, if you include an object literal in the dependencies and that object is recreated on every parent render, the effect will fire endlessly. In those cases, you may need to move the object creation outside the component or memoize it.
Practical Debugging Steps
When you hit this error, the stack trace can look overwhelming because React has already repeated the cycle dozens of times. Start by reading the top of the trace to find which component is named repeatedly. Then look for state setters in these three places:
- The main body of the component, outside any handler or hook.
- JSX event attributes where you might have written
handler()instead ofhandler. useEffecthooks that lack a dependency array or depend on unstable references.
Temporarily comment out each state setter until the error stops. That tells you exactly which update is the culprit. If the setter is inside an effect, ask yourself whether you even need state there. Sometimes developers set local state from props inside an effect when they could simply use the prop directly in the JSX.
The Real Takeaway
The Maximum update depth error is not a mysterious React bug. It is a safety net. It means your component is trying to re-render itself instead of waiting for an external signal. Break the habit of treating renders as events that should produce more state. Treat renders as pure consequences of state, not as causes. Keep state updates inside event handlers, callbacks, or effects with carefully chosen dependencies, and you will never see this error again.
