¿Qué Problema Resuelve Context API?
Imaginemos una aplicación donde tenemos un tema visual (modo claro/oscuro) que debe aplicarse en docenas de componentes dispersos por toda la aplicación. Sin Context, tendríamos que pasar esa prop a través de cada componente intermedio, aunque ese componente no la use directamente:
// Sin Context: Prop Drilling (perforación de props)
function App() {
return <Header theme="dark" />;
}
function Header({ theme }) {
return (
<div>
<Logo theme={theme} />
<Nav theme={theme} />
<UserMenu theme={theme} />
</div>
);
}
function Nav({ theme }) {
return (
<nav>
<Link theme={theme} />
<Link theme={theme} />
</nav>
);
}
// ¡El componente Link recibe theme aunque solo le importe el color del texto!
function Link({ theme }) {
return <a style={{ color: theme === 'dark' ? 'white' : 'black' }}>...</a>;
}
Este patrón se llama prop drilling y genera código redundante, difícil de mantener y propenso a errores cuando necesitas cambiar la estructura de datos.
Los Tres Pasos Fundamentales
- Crear el Context: Usando
React.createContext(), definimos un "tubo" de datos con un valor inicial. - Proveer el Context: Envolvemos la parte de nuestra aplicación que necesita acceso a esos datos con el componente
<MiContext.Provider>. - Consumir el Context: Cualquier componente hijo puede acceder a los datos usando el hook
useContext()o el componente<MiContext.Consumer>.
Creando Nuestro Primer Context: Un Sistema de Autenticación
import { createContext, useContext, useState } from 'react';
// 1. Crear el Context con un valor por defecto
const AuthContext = createContext({
user: null,
login: () => {},
logout: () => {},
isAuthenticated: false
});
// 2. Crear el Provider (componente queprovee los datos)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
const value = {
user,
login,
logout,
isAuthenticated: !!user
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// 3. Hook personalizado para consumir el context
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth debe usarse dentro de AuthProvider');
}
return context;
}
// 4. Usar el hook en cualquier componente
export default function ProfileMenu() {
const { user, logout } = useAuth();
if (!user) return <Link to="/login">Iniciar Sesión</Link>;
return (
<div>
<span>Hola, {user.name}</span>
<button onClick={logout}>Cerrar Sesión</button>
</div>
);
}
useAuth() que envuelva useContext(). Esto facilita cambios futuros y proporciona mensajes de error más claros si el context no está disponible.Patrón Avanzado: Múltiples Contexts Anidados
En aplicaciones reales, tendrás múltiples contextos que coexisten. Aquí un ejemplo de cómo estructurar un sistema de theming completo:
// ThemeContext.js
const ThemeContext = createContext();
const themes = {
light: {
background: '#ffffff',
text: '#333333',
primary: '#007bff',
secondary: '#6c757d'
},
dark: {
background: '#1a1a2e',
text: '#eaeaea',
primary: '#4dabf7',
secondary: '#868e96'
}
};
export function ThemeProvider({ children }) {
const [themeName, setThemeName] = useState('light');
const toggleTheme = () => {
setThemeName(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme: themes[themeName],
themeName,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
// Uso combinado con AuthContext
function Dashboard() {
const { theme } = useTheme();
const { user } = useAuth();
return (
<div style={{ background: theme.background, color: theme.text }}>
<h1>Bienvenido, {user.name}</h1>
<Button variant="primary">Principal</Button>
</div>
);
}
Optimización de Rendimiento
Context tiene un problema de rendimiento conocido: cualquier cambio en el valor del Provider hace que TODOS los componentes consumidores se re-rendericen. Para evitar esto, existen técnicas:
// ❌ Mal: Valor en línea causa re-renders innecesarios
function Parent() {
return (
<MyContext.Provider value={{ count: count, setCount: setCount }}>
<Child />
</MyContext.Provider>
);
}
// ✅ Bien: Memoizar el valor
import { useMemo } from 'react';
function Parent() {
const value = useMemo(() => ({
count,
setCount
}), [count]);
return (
<MyContext.Provider value={value}>
<Child />
</MyContext.Provider>
);
}
// ✅ Alternativa: Dividir el Provider
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={count}>
<SetCountContext.Provider value={setCount}>
{children}
</SetCountContext.Provider>
</CountContext.Provider>
);
}
zustand o jotai ofrecen la misma funcionalidad de estado global pero con mejor rendimiento automático. Context API es ideal para datos que cambian poco (tema, usuario, idioma), pero para datos que cambian frecuentemente, considera alternativas.Patrón de Contexto Escalable: CartContext
Veamos un ejemplo completo y práctico de un carrito de compras:
const CartContext = createContext();
function CartProvider({ children }) {
const [items, setItems] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const addItem = (product) => {
setItems(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const removeItem = (productId) => {
setItems(prev => prev.filter(item => item.id !== productId));
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeItem(productId);
return;
}
setItems(prev => prev.map(item =>
item.id === productId ? { ...item, quantity } : item
));
};
const clearCart = () => setItems([]);
const total = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const totalItems = items.reduce((sum, item) =>
sum + item.quantity, 0
);
const value = {
items,
addItem,
removeItem,
updateQuantity,
clearCart,
total,
totalItems,
isOpen,
openCart: () => setIsOpen(true),
closeCart: () => setIsOpen(false)
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart debe usarse dentro de CartProvider');
}
return context;
};
Cuándo Usar Context y Cuándo No
| Usa Context ✅ | Usa Props o State Local ❌ |
|---|---|
| Datos de autenticación (usuario actual) | Datos específicos de un solo componente |
| Configuración de tema (claro/oscuro) | Estado de un formulario temporal |
| Idioma/región de la aplicación | Items en un dropdown |
| Datos que cambian raramente (settings) | Datos que cambian frecuentemente (posiciones, scrolls) |
| Estado global compartido por muchos componentes | Estado aislado de un componente hijo directo |
src/
├── contexts/
│ ├── AuthContext.jsx # Context + Provider + Hook
│ ├── ThemeContext.jsx
│ └── CartContext.jsx
├── components/
│ ├── Button.jsx
│ ├── Header.jsx
│ └── ProductCard.jsx
├── hooks/
│ └── useLocalStorage.js
└── App.jsx
// En App.jsx:
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<Router>
<Layout />
</Router>
</CartProvider>
</ThemeProvider>
</AuthProvider>
);
}
Este patrón de anidar providers funciona perfectamente porque cada provider solo necesita conocer su propio contexto, no el de sus "padres".
"Context es como el agua: puede fluir a través de cualquier contenedor sin romperlo, pero necesitas canales bien diseñados para que fluya donde debe." — Principio de diseño de React
Errores Comunes y Cómo Evitarlos
UserContext, UserDispatchContext y UserStatusContext, probablemente deberían ser uno solo.createContext() solo se usa cuando el componente está fuera del Provider. Siempre usa el Provider.Resumen y Próximos Pasos
Context API es una herramienta poderosa pero especializada. Ahora que conoces:
- Cómo crear contextos con
createContext() - Cómo proveeer datos con Providers
- Cómo consumirlos con
useContext() - Técnicas de optimización de rendimiento
- Patrones escalables para aplicaciones reales
Estás listo para implementarlo en tus proyectos. Recuerda: usa Context para datos que genuinamente necesitan ser globales, no para todo. El prop drilling en 2-3 niveles es perfectamente aceptable y mantiene el código más predecible.
¿Cuál es la principal desventaja de Context API en términos de rendimiento?
- A) Consume mucha memoria RAM
- B) Hace el código más difícil de leer
- C) Cualquier cambio en el Provider causa re-render en todos los componentes consumidores
- D) No funciona con hooks personalizados
¿Por qué es mejor práctica crear un hook personalizado como useAuth() en lugar de usar useContext(AuthContext) directamente en los componentes?
- A) Es más rápido en ejecución
- B) Permite cambiar el estado global sin re-renders
- C) Proporciona un error claro si se usa fuera del Provider y facilita cambiar la implementación
- D) Reduce el número de líneas de código