Tipando Context
Receta
Crea contextos React completamente tipados con createContext, proveedores tipados y hooks useContext seguros en tipos. Maneja el problema del valor por defecto de forma limpia.
Ejemplo Funcional
// 1. Define el tipo del contexto
type Theme = "light" | "dark";
type ThemeContextType = {
theme: Theme;
toggleTheme: () => void;
};
// 2. Crea el contexto con un valor por defecto sensato o null
const ThemeContext = createContext<ThemeContextType | null>(null);
// 3. Crea un hook tipado con una guarda en tiempo de ejecución
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme debe usarse dentro de ThemeProvider");
}
return context;
}
// 4. Crea el componente proveedor
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 5. Consume en un componente
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>Actual: {theme}</button>
</header>
);
}Profundización
Cómo funciona
createContext<T>(defaultValue)crea un objeto de contexto. El genéricoTdefine la forma del valor que los proveedores deben suministrar y los consumidores recibirán.- El patrón de
null+ hook personalizado evita dos problemas: (1) inventar un valor por defecto falso que podría enmascarar errores, y (2) obligar a los consumidores a verificarundefineden todas partes. - El hook personalizado
useTheme()estrecha el tipo deThemeContextType | nullaThemeContextTypelanzando una excepción si falta el contexto. Esto proporciona a los consumidores un tipo limpio y no nulo. - React re-renderiza todos los consumidores cuando cambia el
valuedel proveedor. Como{ theme, toggleTheme }es un nuevo objeto en cada renderizado, envuélvelo enuseMemosi tienes muchos consumidores.
Variaciones
Contexto con useMemo para valor estable:
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const value = useMemo<ThemeContextType>(
() => ({
theme,
toggleTheme: () => setTheme((prev) => (prev === "light" ? "dark" : "light")),
}),
[theme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Múltiples contextos para preocupaciones separadas:
type AuthContextType = {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth debe usarse dentro de AuthProvider");
return context;
}Contexto con useReducer:
type AppState = { count: number; user: User | null };
type AppAction = { type: "increment" } | { type: "setUser"; payload: User };
type AppContextType = {
state: AppState;
dispatch: React.Dispatch<AppAction>;
};
const AppContext = createContext<AppContextType | null>(null);Notas de TypeScript
React.Dispatch<React.SetStateAction<T>>es el tipo de un setter deuseState. Úsalo en tipos de contexto cuando expongas un setter directamente.React.Dispatch<Action>es el tipo de una función dispatch deuseReducer.- Evita usar
aspara hacer casting del valor por defecto:createContext({} as ThemeContextType)se compila pero proporciona un objeto inválido en tiempo de ejecución si falta el proveedor.
Inconvenientes
- Usar
createContext({} as T)silencia TypeScript pero conduce a bloqueos en tiempo de ejecución cuando falta el proveedor. El patrónnull+ guard es más seguro. - Poner un nuevo objeto en
valueen cada renderizado causa que todos los consumidores se re-rendericen. UsauseMemopara el objeto de valor. - Dividir datos no relacionados en contextos separados previene re-renderizaciones innecesarias. Un único contexto masivo re-renderiza cada consumidor cuando cambia cualquier campo.
- El patrón de hook personalizado (
useTheme,useAuth) es crítico para una buena experiencia de desarrollador. Sin él, los consumidores deben importar tanto el contexto comouseContext, y manejar null ellos mismos.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
null default + guard hook | Seguro en tipos, error claro en mal uso | Requiere hook personalizado por contexto |
Non-null assertion ({} as T) | Sin verificaciones de null necesarias | Bloqueo en tiempo de ejecución si falta el proveedor |
| Valor por defecto real | Funciona sin proveedor | Debes inventar un valor por defecto significativo |
| Zustand o Jotai | API más simple, suscripciones de grano fino | Dependencia externa |
| Estado a nivel de módulo | Sin anidamiento de proveedor | No es reactivo, no es seguro para SSR |
Preguntas Frecuentes
¿Por qué usar null como el valor por defecto del contexto en lugar de un objeto vacío?
createContext({} as T)proporciona un objeto inválido que silenciosamente causa bloqueos en tiempo de ejecución si falta el proveedor.- El valor por defecto
nullcombinado con un hook de guarda proporciona un mensaje de error claro cuando falta el proveedor. - Evita inventar valores por defecto falsos que podrían enmascarar errores.
¿Cómo mejora el patrón de hook personalizado (useTheme, useAuth) la seguridad de tipos?
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme debe usarse dentro de ThemeProvider");
}
return context; // estrechado a ThemeContextType
}- Estrecha el tipo de
T | nullaTcon una única guarda en tiempo de ejecución. - Los consumidores obtienen un tipo limpio y no nulo sin verificaciones de null.
¿Por qué poner un nuevo objeto en el value del Proveedor en cada renderizado causa problemas de rendimiento?
- React re-renderiza todos los consumidores cuando cambia la referencia del
valuedel proveedor. { theme, toggleTheme }crea un nuevo objeto en cada renderizado, lo que provoca re-renderizaciones.- Envuelve el valor en
useMemopara producir una referencia estable.
¿Cuándo deberías dividir el estado en contextos separados?
- Cuando datos no relacionados se agrupan en un único contexto, todos los consumidores se re-renderizan cuando cambia cualquier campo.
- Contextos separados (p. ej.,
AuthContext,ThemeContext) previenen re-renderizaciones innecesarias. - Divide cuando los consumidores solo necesitan un subconjunto de los datos.
¿Cuál es el tipo de un setter de useState cuando se expone a través del contexto?
React.Dispatch<React.SetStateAction<T>>.- Esto permite tanto valores directos como funciones actualizadoras:
setState(5)ysetState(prev => prev + 1).
Inconveniente: ¿Qué sucede si usas as T para hacer casting del valor por defecto del contexto?
createContext({} as ThemeContextType)se compila pero proporciona un objeto sin propiedades reales.- Si un componente se renderiza fuera del proveedor, obtiene el objeto vacío y se bloquea en tiempo de ejecución.
- El patrón
null+ guard es estrictamente más seguro.
¿Cómo tipas un contexto que usa useReducer?
type AppContextType = {
state: AppState;
dispatch: React.Dispatch<AppAction>;
};
const AppContext = createContext<AppContextType | null>(null);React.Dispatch<Action>es el tipo de una función dispatch deuseReducer.- El contexto lleva tanto el estado como dispatch a los consumidores.
¿Cuándo deberías usar Zustand o Jotai en lugar de React Context?
- Cuando necesitas suscripciones de grano fino (solo re-renderizar cuando campos específicos cambian).
- Cuando la complejidad de la API de proveedores, hooks personalizados y
useMemose vuelve excesiva. - Context es suficiente para actualizaciones de baja frecuencia como estado de tema o autenticación.
Inconveniente: ¿Puedes usar un valor por defecto real en createContext para evitar el patrón null?
- Sí, si puedes proporcionar un valor por defecto significativo que sea seguro usar sin un proveedor.
- Esto elimina la necesidad de un hook de guarda personalizado.
- Sin embargo, inventar valores por defecto plausibles (como funciones sin operación) puede ocultar errores cuando el proveedor se omite accidentalmente.
¿Cómo estabilizas el valor del contexto con useMemo?
const value = useMemo<ThemeContextType>(
() => ({
theme,
toggleTheme: () => setTheme((p) => (p === "light" ? "dark" : "light")),
}),
[theme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;useMemoasegura que el objeto de valor solo cambia cuando cambian las dependencias.- Esto previene re-renderizaciones innecesarias de consumidores.