¿Cuáles son las reglas de los hooks y por qué existen? ¿Qué pasa si llamo un hook dentro de un if?
Ver respuesta — intenta responderla en voz alta primero
Hay dos reglas: llamar los hooks solo en el nivel superior del componente (nunca dentro de condicionales, bucles o funciones anidadas) y llamarlos solo desde componentes de React o desde custom hooks. Existen porque React identifica cada hook por su ORDEN de llamada; si el orden cambia entre renders, React mezcla los estados.
Cuando escribes useState("Ana"), no le pasas a React ningún nombre ni ID. Entonces, ¿cómo sabe React qué estado te corresponde en el siguiente render? Por la posición: React guarda los hooks de cada componente en una lista y los va entregando en orden. Primer hook llamado, primera casilla; segundo hook, segunda casilla.
Los puntos clave:
- Regla 1: solo en el nivel superior. Nada de hooks dentro de
if,for,while, callbacks o después de unreturntemprano. Así el componente llama SIEMPRE los mismos hooks en el MISMO orden. - Regla 2: solo en componentes o custom hooks. No en funciones normales de JavaScript ni en clases, porque solo dentro de un render React tiene esa lista preparada.
- El porqué: si un render llama 3 hooks y el siguiente llama 2 (porque un if se saltó uno), la lista se desalinea: el estado de la casilla 2 se entrega al hook equivocado. React detecta el cambio de orden y lanza un error.
- La condición va DENTRO del hook, no alrededor. En vez de condicionar la llamada a
useEffect, llama siempre al hook y pon elifdentro de su función. - El plugin
eslint-plugin-react-hooksdetecta estas violaciones automáticamente; en entrevista suma mencionarlo.
import { useEffect, useState } from "react";
type Props = { usuarioId: number | null };
function PerfilUsuario({ usuarioId }: Props) {
// Nivel superior: estos hooks corren en TODOS los renders,
// siempre en el mismo orden (casillas 1 y 2 de la lista interna)
const [nombre, setNombre] = useState<string | null>(null);
useEffect(() => {
// La condición vive DENTRO del efecto, no alrededor del hook
if (usuarioId === null) return;
const controlador = new AbortController();
fetch(`/api/usuarios/${usuarioId}`, { signal: controlador.signal })
.then(res => res.json())
.then((datos: { nombre: string }) => setNombre(datos.nombre))
.catch(() => {}); // ignoramos la cancelación
return () => controlador.abort(); // cleanup: cancela peticiones viejas
}, [usuarioId]);
// Los returns tempranos van DESPUÉS de todos los hooks
if (usuarioId === null) return <p>Selecciona un usuario</p>;
return <h2>{nombre ?? "Cargando..."}</h2>;
}
Condicionar la llamada de un hook, cambiando cuántos hooks corren en cada render:
// MAL: en unos renders hay 2 hooks y en otros solo 1. React desalinea
// su lista interna y lanza "Rendered fewer hooks than expected"
function Perfil({ activo }: { activo: boolean }) {
if (activo) {
const [nombre] = useState("Ana"); // hook dentro de un if
}
const [edad] = useState(30);
}
// BIEN: los hooks siempre corren; el return temprano va después
function PerfilCorregido({ activo }: { activo: boolean }) {
const [nombre] = useState("Ana");
const [edad] = useState(30);
if (!activo) return null;
return <p>{nombre}, {edad}</p>;
}
Variante del mismo error: un return temprano ANTES de un useEffect; en los renders donde se ejecuta el return, ese effect desaparece y el orden se rompe igual.
Son dos reglas: los hooks solo se llaman en el nivel superior del componente, nunca dentro de condicionales, bucles o después de un return temprano; y solo se llaman desde componentes de React o desde custom hooks. La razón es que React no identifica los hooks por nombre sino por orden de llamada: guarda una lista por componente y en cada render la recorre en el mismo orden. Si un if se salta un hook, la lista se desalinea y un estado termina asignado al hook equivocado, por eso React lanza un error. En la práctica, la condición va dentro del hook y el linter de react-hooks me avisa si rompo la regla.
Este componente rompe las reglas de los hooks de dos formas. Encuéntralas y corrígelo:
function Notificaciones({ visible }: { visible: boolean }) {
if (!visible) return null;
const [cantidad, setCantidad] = useState(0);
useEffect(() => { document.title = `(${cantidad}) notificaciones`; }, [cantidad]);
return <button onClick={() => setCantidad(c => c + 1)}>+1</button>;
}
Ver solución
import { useEffect, useState } from "react";
// Problema: el return temprano estaba ANTES de los hooks, así que useState
// y useEffect se saltaban cuando visible era false y el orden cambiaba.
function Notificaciones({ visible }: { visible: boolean }) {
const [cantidad, setCantidad] = useState(0); // 1. hooks primero, siempre
useEffect(() => {
if (!visible) return; // 2. la condición vive dentro del efecto
document.title = `(${cantidad}) notificaciones`;
}, [visible, cantidad]);
if (!visible) return null; // 3. el return temprano va después de los hooks
return <button onClick={() => setCantidad(c => c + 1)}>+1</button>;
}