React
useContext
Explica cómo useContext permite leer valores compartidos sin prop drilling, además de sus límites, alcance, providers y efectos sobre el renderizado.
- Última actualización
- Actualizada
- Nivel
- Aplicación
React
Explica cómo useContext permite leer valores compartidos sin prop drilling, además de sus límites, alcance, providers y efectos sobre el renderizado.
useContext permite leer el valor del provider más cercano y volver a renderizar el consumidor cuando ese valor cambia. Context distribuye datos a través del árbol; no decide por sí solo cómo se actualizan ni reemplaza automáticamente una store.
El flujo normal de React es explícito:
parent
↓ props
child
↓ props
grandchildCuando muchos niveles intermedios solo reenvían el mismo valor, Context permite que una rama lo lea directamente:
Provider
├─ Toolbar
│ └─ ThemeButton → useContext
└─ Content
└─ Article → useContextimport { createContext, useContext, useMemo, useState } from "react";
type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);El valor pasado a createContext no es un valor inicial que luego cambia. Es un fallback estático utilizado cuando no existe un provider superior.
En React moderno puedes usar el contexto directamente como provider:
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const value = useMemo(
() => ({ theme, setTheme }),
[theme],
);
return <ThemeContext value={value}>{children}</ThemeContext>;
}En versiones anteriores y en mucho código existente verás:
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>Ambas formas expresan el mismo concepto de provider.
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}El custom hook centraliza el error y evita que cada consumidor repita la comprobación.
function ThemeButton() {
const { theme, setTheme } = useTheme();
return (
<button
type="button"
onClick={() => setTheme((current) => (current === "light" ? "dark" : "light"))}
>
Usar tema {theme === "light" ? "oscuro" : "claro"}
</button>
);
}useContext lee el provider más cercano por encima del componente:
<ThemeContext value={{ theme: "light", setTheme }}>
<Header />
<ThemeContext value={{ theme: "dark", setTheme }}>
<Preview />
</ThemeContext>
</ThemeContext>Header recibe light; Preview recibe dark.
Context no es necesariamente global. Puede configurar una única sección, widget o compound component.
Context transporta un valor. El state puede venir de:
useState.useReducer.function AuthProvider({ session, children }: AuthProviderProps) {
return <AuthContext value={session}>{children}</AuthContext>;
}Aquí Context distribuye la sesión, pero no la crea ni la persiste.
Prop drilling no significa “pasar cualquier prop por dos componentes”. A veces la dependencia explícita es más fácil de entender:
function Page({ user }: { user: User }) {
return <UserMenu user={user} />;
}Antes de crear Context considera composición:
function Layout({ userMenu }: { userMenu: React.ReactNode }) {
return <header>{userMenu}</header>;
}
<Layout userMenu={<UserMenu user={user} />} />La composición evita que Layout conozca el contrato de User.
Context aporta cuando múltiples descendientes necesitan una dependencia compartida o cuando forma parte natural del entorno del árbol.
Context suele funcionar bien para:
type CartAction =
| { type: "itemAdded"; product: Product }
| { type: "itemRemoved"; productId: string };
type CartContextValue = {
state: CartState;
dispatch: React.Dispatch<CartAction>;
};
const CartContext = createContext<CartContextValue | null>(null);
function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, initialCartState);
const value = useMemo(() => ({ state, dispatch }), [state]);
return <CartContext value={value}>{children}</CartContext>;
}Esto centraliza transiciones de una feature. No significa que todo el estado de la aplicación deba vivir en un único reducer global.
Cuando el value del provider cambia según Object.is, los consumidores vuelven a renderizar.
<SettingsContext value={{ theme, locale }}>El objeto es nuevo en cada render. Esto no siempre es un problema, pero puede ampliar actualizaciones si el provider renderiza con frecuencia.
Una opción es estabilizar el valor cuando existe una necesidad medida:
const value = useMemo(() => ({ theme, locale }), [theme, locale]);useMemo no evita el render del provider ni garantiza que ningún consumidor renderice. Solo conserva la referencia mientras sus dependencias sean iguales.
Si theme y session cambian con frecuencias y consumidores distintos, separarlos reduce dependencias:
<ThemeContext value={theme}>
<SessionContext value={session}>{children}</SessionContext>
</ThemeContext>También puedes separar datos y acciones:
<CartStateContext value={state}>
<CartDispatchContext value={dispatch}>{children}</CartDispatchContext>
</CartStateContext>Los componentes que solo despachan acciones no necesitan consumir el objeto completo del state.
Un valor que cambia muchas veces por segundo y tiene numerosos consumidores puede generar trabajo amplio. Ejemplos:
En esos casos evalúa:
Context no ofrece por sí solo:
Puedes distribuir un cliente o configuración mediante Context, pero copiar toda respuesta remota a un contexto no crea una estrategia de server state.
Context puede comunicar la sesión actual a la UI:
const { user } = useSession();Esto permite mostrar navegación o estados visuales. No constituye autorización.
El servidor debe verificar permisos en cada operación protegida. Ocultar un botón no protege el endpoint.
Varios providers no son automáticamente un problema. Se vuelven difíciles cuando:
Puedes crear un componente de composición:
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<SessionProvider>
<NotificationProvider>{children}</NotificationProvider>
</SessionProvider>
</ThemeProvider>
);
}Esto mejora el punto de entrada, pero no corrige responsabilidades mal definidas.
Un default real puede ser útil para configuración opcional:
const DensityContext = createContext<"comfortable" | "compact">("comfortable");Para un provider obligatorio, null más un hook que lanza un error evita fallos silenciosos.
No uses un objeto falso con funciones vacías si eso ocultará que el provider no existe.
Una librería puede utilizar Context para coordinar componentes que comparten una instancia:
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Resumen</TabsTrigger>
</TabsList>
<TabsPanel value="overview">...</TabsPanel>
</Tabs>El contexto interno permite que triggers y panels conozcan el estado de Tabs sin exponer props manuales entre cada nivel.
La API pública continúa siendo declarativa y accesible.
value por costumbre sin medir ni entender consumers.useContext lee el provider más cercano.value vuelve a renderizar consumers.Context: diseño de providers y rendimiento profundiza en límites de providers, separación de dependencias y estrategias para evitar contextos demasiado amplios.