¿Qué es un custom hook y cuándo crearías uno? ¿Dos componentes que usan el mismo custom hook comparten estado?
Ver respuesta — intenta responderla en voz alta primero
Un custom hook es una función que empieza con use y que llama a otros hooks por dentro, para extraer lógica con estado y reutilizarla entre componentes. Lo que se comparte es la LÓGICA, no el estado: cada componente que lo llama recibe su propia copia independiente del estado.
Cuando dos o tres componentes repiten el mismo patrón de useState + useEffect (escuchar el tamaño de la ventana, leer localStorage, hacer debounce), esa lógica se puede extraer a una función. Como esa función llama hooks, debe cumplir las reglas de los hooks, y por convención (que el linter usa para verificarte) su nombre empieza con use.
Los puntos clave:
- Es solo una función de JavaScript que usa hooks adentro. No hay magia: React la trata como parte del componente que la llama.
- Convención
use*: obligatoria en la práctica; le dice al linter (y a otros devs) que aplican las reglas de los hooks. - Comparte lógica, NO estado. Cada llamada al hook crea instancias nuevas de sus useState/useEffect. Si
ComponenteAyComponenteBusanuseContador(), cada uno tiene su propio contador. Para compartir estado real necesitas Context o levantar el estado. - Cuándo crear uno: cuando repites lógica con estado en 2+ componentes, o cuando un componente crece tanto que separar la lógica de la vista lo hace legible y testeable.
- Cuándo NO: si la función no usa hooks (es una utilidad pura como
formatearFecha), no la llamesuseAlgo; es solo una función normal.
import { useEffect, useState } from "react";
// Custom hook: encapsula la suscripción al resize de la ventana
function useVentanaAncho(): number {
const [ancho, setAncho] = useState<number>(() => window.innerWidth);
useEffect(() => {
const alRedimensionar = () => setAncho(window.innerWidth);
window.addEventListener("resize", alRedimensionar);
// Cleanup: el hook limpia lo que creó
return () => window.removeEventListener("resize", alRedimensionar);
}, []);
return ancho;
}
function Encabezado() {
const ancho = useVentanaAncho(); // instancia propia
return <h1>{ancho < 768 ? "Menú compacto" : "Menú completo"}</h1>;
}
function PieDePagina() {
const ancho = useVentanaAncho(); // OTRA instancia, estado independiente
return <footer>Ancho actual: {ancho}px</footer>;
}
// Comportamiento esperado: ambos reaccionan al resize, pero cada uno
// mantiene SU propio estado: comparten la lógica, no la variable "ancho".
Creer que el custom hook es un almacén global y que todos los componentes ven el mismo estado:
function useContador() {
const [valor, setValor] = useState(0);
return { valor, incrementar: () => setValor(v => v + 1) };
}
// MAL (como expectativa): "si incremento en A, B también cambia"
function A() {
const { valor, incrementar } = useContador();
return <button onClick={incrementar}>A: {valor}</button>;
}
function B() {
const { valor } = useContador();
return <p>B: {valor}</p>; // B SIEMPRE muestra 0: es otra instancia
}
// BIEN: para compartir el MISMO estado, se levanta al padre (o Context)
function Padre() {
const { valor, incrementar } = useContador(); // una sola instancia
return <><button onClick={incrementar}>A: {valor}</button><p>B: {valor}</p></>;
}
Otro tropiezo: llamar useAlgo a una función que no usa hooks, o llamar hooks dentro de una función sin el prefijo use; el linter necesita la convención para protegerte.
Un custom hook es una función que empieza con
usey llama hooks por dentro, y sirve para extraer lógica con estado y reutilizarla, por ejemplo unuseVentanaAnchoo unuseLocalStorage. Crearía uno cuando veo el mismo patrón de useState y useEffect repetido en varios componentes, o para separar la lógica de la vista y poder testearla. El matiz importante es que comparte lógica, no estado: cada componente que llama el hook obtiene su propia instancia de estado independiente. Si necesito que varios componentes vean el mismo dato, levanto el estado a un padre o uso Context.
Implementa useLocalStorage<T>(clave: string, valorInicial: T) que devuelva [valor, setValor] como useState, pero persistiendo el valor en localStorage.
Ver solución
import { useState } from "react";
function useLocalStorage<T>(clave: string, valorInicial: T): [T, (nuevo: T) => void] {
// Inicialización perezosa: leemos localStorage solo una vez
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];
}
// Uso: misma forma que useState, pero sobrevive recargas de página
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>;
}