Tema compartido con Context y useTheme
A veces varios componentes necesitan el mismo dato. Puedes pasarlo por props o utilizar Context para ofrecerlo a un grupo de descendientes. Vamos a compartir un tema claro u oscuro.
useTheme no es un Hook incorporado en React. Lo crearemos nosotros para leer un contexto. Necesitamos tres archivos, además del componente principal.
1. Crear el contexto #
Crea src/contexts/ThemeContext.ts:
1import { createContext, type Dispatch, type SetStateAction } from 'react'; 2 3export type Theme = 'light' | 'dark'; 4 5interface ThemeContextValue { 6 theme: Theme; 7 setTheme: Dispatch<SetStateAction<Theme>>; 8} 9 10export const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
El tipo limita el tema a dos valores. undefined representa la ausencia de proveedor para poder detectar un uso incorrecto.
2. Crear el proveedor #
En src/contexts/ThemeProvider.tsx:
1import { useState, type ReactNode } from 'react'; 2import { ThemeContext, type Theme } from './ThemeContext'; 3 4export function ThemeProvider({ children }: { children: ReactNode }) { 5 const [theme, setTheme] = useState<Theme>('light'); 6 7 return ( 8 <ThemeContext.Provider value={{ theme, setTheme }}> 9 <div 10 style={{ 11 backgroundColor: theme === 'dark' ? '#171717' : '#ffffff', 12 color: theme === 'dark' ? '#ffffff' : '#171717', 13 padding: 16, 14 }} 15 > 16 {children} 17 </div> 18 </ThemeContext.Provider> 19 ); 20}
El proveedor contiene el estado que se va a compartir. Cambiamos los estilos explícitamente: un contexto con la cadena 'dark' no recolorea la página por sí solo.
3. Crear el Hook #
En src/hooks/useTheme.ts:
1import { useContext } from 'react'; 2import { ThemeContext } from '../contexts/ThemeContext'; 3 4export function useTheme() { 5 const context = useContext(ThemeContext); 6 if (context === undefined) { 7 throw new Error('useTheme debe utilizarse dentro de ThemeProvider'); 8 } 9 return context; 10}
La comprobación ofrece un error claro si olvidas el proveedor.
4. Consumir el mismo estado en dos componentes #
En src/App.tsx:
1import { ThemeProvider } from './contexts/ThemeProvider'; 2import { useTheme } from './hooks/useTheme'; 3 4function A() { 5 const { theme, setTheme } = useTheme(); 6 return ( 7 <> 8 <p>Componente A: {theme}</p> 9 <button onClick={() => setTheme('dark')}>Tema oscuro</button> 10 <button onClick={() => setTheme('light')}>Tema claro</button> 11 </> 12 ); 13} 14 15function B() { 16 const { theme } = useTheme(); 17 return <p>Componente B: {theme}</p>; 18} 19 20export default function Ejem_23() { 21 return <ThemeProvider><A /><B /></ThemeProvider>; 22}
A y B leen el valor del proveedor más cercano por encima de ellos. Si A cambia el tema, B recibe el nuevo valor. Dos proveedores distintos tendrían estados independientes.
Prueba tú #
Alterna entre claro y oscuro y comprueba que cambia tanto el texto de A y B como el aspecto del contenedor. El tema vuelve a claro al recargar porque este ejemplo no lo guarda de forma persistente.
Consulta la referencia oficial de useContext.
Cuestionario del tema #
Abre el cuestionario en las actividades de este tema para comprobar lo aprendido. Cada respuesta incluye una explicación.