
Los hooks de React están escritos con genéricos de TypeScript, de modo que el editor conoce el tipo del valor que devuelven y de los argumentos que aceptan. Entender cómo se activan esos genéricos permite escribir componentes con estado complejo sin sacrificar el autocompletado ni la detección temprana de errores. En esta lección vamos a repasar el tipado práctico de useState, useRef, useEffect y useContext, que son los cuatro hooks que aparecen en prácticamente todas las aplicaciones profesionales.
Tipar useState con inferencia y con genéricos
El hook useState es el primer caso de uso de genéricos. Cuando pasas un valor inicial, TypeScript infiere el tipo del estado a partir de ese valor y del setter.
import { useState } from "react";
export function Contador() {
const [contador, setContador] = useState(0); // number inferido
return (
<button onClick={() => setContador((c) => c + 1)}>
Clics: {contador}
</button>
);
}
La inferencia funciona bien cuando el valor inicial describe todo el dominio del estado. Cuando el estado puede contener valores de otro tipo (por ejemplo, null hasta que llega la respuesta del servidor), la inferencia se queda corta y se específica el genérico de forma explícita.
import { useState } from "react";
interface Usuario {
id: number;
nombre: string;
}
export function PerfilUsuario() {
const [usuario, setUsuario] = useState<Usuario | null>(null);
function cargar() {
setUsuario({ id: 1, nombre: "Ana Martín" });
}
return usuario ? <p>{usuario.nombre}</p> : <button onClick={cargar}>Cargar</button>;
}
Al escribir useState<Usuario | null>(null), el setter setUsuario acepta un Usuario, acepta null y rechaza cualquier otro valor. Ese contrato evita estados inconsistentes como asignar un string por error.
Para estados tipo formulario declara la interfaz del modelo una sola vez y reutilízala tanto en
useStatecomo en las funciones que procesan el envío. Así los cambios de contrato se propagan en un solo punto.
Actualizaciones funcionales y arrays
El setter acepta un valor directo o una función actualizadora que recibe el estado previo. TypeScript comprueba que la función devuelva un valor del mismo tipo.
import { useState } from "react";
export function ListaTareas() {
const [tareas, setTareas] = useState<string[]>([]);
function agregar(nueva: string) {
setTareas((prev) => [...prev, nueva]);
}
return <p>Total: {tareas.length}</p>;
}
Si quitaras el genérico <string[]>, TypeScript inferiría never[] a partir del array vacío y el setter rechazaría cualquier adición. Esta es una de las trampas más frecuentes en proyectos que migran de JavaScript a TypeScript, y conviene explicitar siempre el genérico cuando el valor inicial sea una colección vacía.
Tipar useRef para DOM y valores mutables
El hook useRef tiene dos usos distintos y cada uno se tipa de forma ligeramente diferente. Cuando la referencia apunta a un elemento del DOM, el genérico es el tipo del elemento HTML y el valor inicial es null.
import { useRef, useEffect } from "react";
export function CampoAutofoco() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Escribe aquí" />;
}
El operador ?. es obligatorio porque inputRef.current puede ser null mientras React no haya montado el componente. TypeScript fuerza esa comprobación porque el tipo resultante es HTMLInputElement | null.
El segundo uso de useRef es mantener un valor mutable que persiste entre renderizados sin provocar una nueva renderización. En ese caso el genérico describe el tipo del valor y el valor inicial coincide con ese tipo.
import { useRef, useEffect } from "react";
export function Cronometro() {
const iniciado = useRef<number>(Date.now());
useEffect(() => {
console.log("Montado hace", Date.now() - iniciado.current, "ms");
}, []);
return <p>Cronómetro activo</p>;
}
Tipar useEffect y sus dependencias
El hook useEffect no necesita genéricos porque no devuelve ningún valor, pero su firma impone dos reglas que TypeScript ayuda a respetar. La función de efecto debe devolver void o una función de limpieza (sin valor devuelto), y el array de dependencias debe contener variables del mismo scope.
import { useEffect, useState } from "react";
interface Post {
id: number;
title: string;
}
export function UltimosPosts() {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
const controller = new AbortController();
fetch("/api/posts", { signal: controller.signal })
.then((res) => res.json())
.then((data: Post[]) => setPosts(data));
return () => controller.abort();
}, []);
return <p>Total de posts: {posts.length}</p>;
}
La anotación (data: Post[]) informa a TypeScript de la forma del JSON que devuelve la API. En proyectos reales esa conversión suele hacerse con una librería de validación como Zod para confirmar la forma en tiempo de ejecución, pero el patrón base es el mismo.
Tipar useContext con valores no nulos
El hook useContext requiere que el contexto tenga un valor por defecto compatible con el tipo declarado. Para contextos que solo tienen sentido dentro de un Provider se declara el tipo como T | null y se envuelve en un hook personalizado que lanza un error si el valor es nulo.
// TemaContext.tsx
import { createContext, useContext, useState, type ReactNode } from "react";
interface TemaContextValue {
modo: "claro" | "oscuro";
alternar: () => void;
}
const TemaContext = createContext<TemaContextValue | null>(null);
export function TemaProvider({ children }: { children: ReactNode }) {
const [modo, setModo] = useState<"claro" | "oscuro">("claro");
const alternar = () => setModo((prev) => (prev === "claro" ? "oscuro" : "claro"));
return <TemaContext.Provider value={{ modo, alternar }}>{children}</TemaContext.Provider>;
}
export function useTema() {
const valor = useContext(TemaContext);
if (!valor) {
throw new Error("useTema debe usarse dentro de TemaProvider");
}
return valor;
}
El hook personalizado useTema resuelve dos problemas al mismo tiempo. Garantiza que el consumidor siempre recibe un valor no nulo (gracias al throw) y centraliza el mensaje de error para que cualquier uso incorrecto se detecte al instante durante el desarrollo. Este patrón es el que aplican equipos de consultoría al construir librerías internas de contextos reutilizables.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, React es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de React
Explora más contenido relacionado con React y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Declarar el tipo del estado en useState con inferencia o con genéricos. Tipar referencias a elementos del DOM con useRef. Crear contextos tipados con useContext y valores no nulos. Aplicar tipos a dependencias y retornos de useEffect.