← React & TypeScript

Hooks

What is a custom hook and when would you create one? Do two components that use the same custom hook share state?

Show answer — try answering out loud first

A custom hook is a function that starts with use and calls other hooks inside it, to extract stateful logic and reuse it across components. What's shared is the LOGIC, not the state: each component that calls it gets its own independent copy of the state.

When two or three components repeat the same useState + useEffect pattern (listening for the window size, reading localStorage, debouncing), that logic can be extracted into a function. Since that function calls hooks, it must follow the rules of hooks, and by convention (which the linter uses to check you) its name starts with use.

The key points:

  • It's just a JavaScript function that uses hooks inside it. There's no magic: React treats it as part of the component that calls it.
  • use* convention: mandatory in practice; it tells the linter (and other devs) that the rules of hooks apply.
  • It shares logic, NOT state. Each call to the hook creates new instances of its useState/useEffect. If ComponenteA and ComponenteB use useContador(), each one has its own counter. To share real state you need Context or to lift the state up.
  • When to create one: when you repeat stateful logic in 2+ components, or when a component grows so much that separating the logic from the view makes it readable and testable.
  • When NOT to: if the function doesn't use hooks (it's a pure utility like formatearFecha), don't call it useSomething; it's just a normal function.
import { useEffect, useState } from "react";

// Custom hook: encapsulates the subscription to the window resize
function useVentanaAncho(): number {
  const [ancho, setAncho] = useState<number>(() => window.innerWidth);

  useEffect(() => {
    const alRedimensionar = () => setAncho(window.innerWidth);
    window.addEventListener("resize", alRedimensionar);
    // Cleanup: the hook cleans up what it created
    return () => window.removeEventListener("resize", alRedimensionar);
  }, []);

  return ancho;
}

function Encabezado() {
  const ancho = useVentanaAncho(); // its own instance
  return <h1>{ancho < 768 ? "Menú compacto" : "Menú completo"}</h1>;
}
function PieDePagina() {
  const ancho = useVentanaAncho(); // ANOTHER instance, independent state
  return <footer>Ancho actual: {ancho}px</footer>;
}
// Expected behavior: both react to the resize, but each one
// keeps ITS own state: they share the logic, not the "ancho" variable.

Believing that the custom hook is a global store and that all components see the same state:

function useContador() {
  const [valor, setValor] = useState(0);
  return { valor, incrementar: () => setValor(v => v + 1) };
}
// BAD (as an expectation): "if I increment in A, B also changes"
function A() {
  const { valor, incrementar } = useContador();
  return <button onClick={incrementar}>A: {valor}</button>;
}
function B() {
  const { valor } = useContador();
  return <p>B: {valor}</p>; // B ALWAYS shows 0: it's another instance
}

// GOOD: to share the SAME state, lift it up to the parent (or Context)
function Padre() {
  const { valor, incrementar } = useContador(); // a single instance
  return <><button onClick={incrementar}>A: {valor}</button><p>B: {valor}</p></>;
}

Another stumble: naming a function useSomething when it doesn't use hooks, or calling hooks inside a function without the use prefix; the linter needs the convention to protect you.

A custom hook is a function that starts with use and calls hooks inside it, and it's for extracting stateful logic and reusing it, for example a useVentanaAncho or a useLocalStorage. I would create one when I see the same useState and useEffect pattern repeated in several components, or to separate the logic from the view and be able to test it. The important nuance is that it shares logic, not state: each component that calls the hook gets its own independent state instance. If I need several components to see the same data, I lift the state up to a parent or use Context.

Quick challenge

Implement useLocalStorage<T>(clave: string, valorInicial: T) that returns [valor, setValor] like useState, but persisting the value in localStorage.

See solution
import { useState } from "react";

function useLocalStorage<T>(clave: string, valorInicial: T): [T, (nuevo: T) => void] {
  // Lazy initialization: we read localStorage only once
  const [valor, setValorInterno] = useState<T>(() => {
    const guardado = localStorage.getItem(clave);
    return guardado !== null ? (JSON.parse(guardado) as T) : valorInicial;
  });

  function setValor(nuevo: T) {
    setValorInterno(nuevo);
    localStorage.setItem(clave, JSON.stringify(nuevo));
  }

  return [valor, setValor];
}

// Usage: same shape as useState, but survives page reloads
function Preferencias() {
  const [tema, setTema] = useLocalStorage<"claro" | "oscuro">("tema", "claro");
  const alternar = () => setTema(tema === "claro" ? "oscuro" : "claro");
  return <button onClick={alternar}>Tema actual: {tema}</button>;
}