← React & TypeScript

Hooks

What are the rules of hooks and why do they exist? What happens if I call a hook inside an if?

Show answer — try answering out loud first

There are two rules: call hooks only at the top level of the component (never inside conditionals, loops, or nested functions) and call them only from React components or from custom hooks. They exist because React identifies each hook by its call ORDER; if the order changes between renders, React mixes up the states.

When you write useState("Ana"), you don't pass React any name or ID. So how does React know which state corresponds to you on the next render? By position: React stores each component's hooks in a list and hands them out in order. First hook called, first slot; second hook, second slot.

The key points:

  • Rule 1: only at the top level. No hooks inside if, for, while, callbacks, or after an early return. That way the component ALWAYS calls the same hooks in the SAME order.
  • Rule 2: only in components or custom hooks. Not in normal JavaScript functions or in classes, because only during a render does React have that list prepared.
  • The why: if one render calls 3 hooks and the next calls 2 (because an if skipped one), the list gets misaligned: the state of slot 2 is handed to the wrong hook. React detects the order change and throws an error.
  • The condition goes INSIDE the hook, not around it. Instead of conditioning the call to useEffect, always call the hook and put the if inside its function.
  • The eslint-plugin-react-hooks plugin detects these violations automatically; mentioning it earns points in an interview.
import { useEffect, useState } from "react";

type Props = { usuarioId: number | null };

function PerfilUsuario({ usuarioId }: Props) {
  // Top level: these hooks run on ALL renders,
  // always in the same order (slots 1 and 2 of the internal list)
  const [nombre, setNombre] = useState<string | null>(null);

  useEffect(() => {
    // The condition lives INSIDE the effect, not around the 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(() => {}); // we ignore the cancellation

    return () => controlador.abort(); // cleanup: cancels old requests
  }, [usuarioId]);

  // Early returns go AFTER all the hooks
  if (usuarioId === null) return <p>Selecciona un usuario</p>;

  return <h2>{nombre ?? "Cargando..."}</h2>;
}

Conditioning a hook call, changing how many hooks run on each render:

// BAD: in some renders there are 2 hooks and in others only 1. React misaligns
// its internal list and throws "Rendered fewer hooks than expected"
function Perfil({ activo }: { activo: boolean }) {
  if (activo) {
    const [nombre] = useState("Ana"); // hook inside an if
  }
  const [edad] = useState(30);
}

// GOOD: the hooks always run; the early return goes after
function PerfilCorregido({ activo }: { activo: boolean }) {
  const [nombre] = useState("Ana");
  const [edad] = useState(30);
  if (!activo) return null;
  return <p>{nombre}, {edad}</p>;
}

Variant of the same error: an early return BEFORE a useEffect; in the renders where the return runs, that effect disappears and the order breaks all the same.

There are two rules: hooks are only called at the top level of the component, never inside conditionals, loops, or after an early return; and they are only called from React components or from custom hooks. The reason is that React doesn't identify hooks by name but by call order: it keeps a list per component and on each render traverses it in the same order. If an if skips a hook, the list gets misaligned and a state ends up assigned to the wrong hook, which is why React throws an error. In practice, the condition goes inside the hook and the react-hooks linter warns me if I break the rule.

Quick challenge

This component breaks the rules of hooks in two ways. Find them and fix it:

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>;
}
See solution
import { useEffect, useState } from "react";

// Problem: the early return was BEFORE the hooks, so useState
// and useEffect were skipped when visible was false and the order changed.
function Notificaciones({ visible }: { visible: boolean }) {
  const [cantidad, setCantidad] = useState(0); // 1. hooks first, always

  useEffect(() => {
    if (!visible) return; // 2. the condition lives inside the effect
    document.title = `(${cantidad}) notificaciones`;
  }, [visible, cantidad]);

  if (!visible) return null; // 3. the early return goes after the hooks

  return <button onClick={() => setCantidad(c => c + 1)}>+1</button>;
}