Contexto
El portfolio maneja dos dimensiones ortogonales: tema (editorial/terminal) y modo (light/dark). Podria haber creado dos contexts separados, pero un solo provider con dos estados resulta mas simple.
Lo que aprendi
Un ThemeProvider con dos estados independientes y setters estables con useCallback:
"use client";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
type Theme = "editorial" | "terminal";
type Mode = "light" | "dark";
type ThemeContextType = {
theme: Theme;
mode: Mode;
setTheme: (t: Theme) => void;
toggleMode: () => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("editorial");
const [mode, setMode] = useState<Mode>("dark");
// Leer de localStorage al montar
useEffect(() => {
const savedTheme = localStorage.getItem("site-theme") as Theme | null;
const savedMode = localStorage.getItem("site-mode") as Mode | null;
if (savedTheme) setThemeState(savedTheme);
if (savedMode) setMode(savedMode);
}, []);
// Sincronizar clases en <html> y persistir
useEffect(() => {
const html = document.documentElement;
html.classList.remove("theme-editorial", "theme-terminal");
html.classList.add(`theme-${theme}`);
localStorage.setItem("site-theme", theme);
}, [theme]);
useEffect(() => {
const html = document.documentElement;
html.classList.toggle("dark", mode === "dark");
localStorage.setItem("site-mode", mode);
}, [mode]);
const setTheme = useCallback((t: Theme) => setThemeState(t), []);
const toggleMode = useCallback(
() => setMode((m) => (m === "dark" ? "light" : "dark")),
[]
);
return (
<ThemeContext.Provider value={{ theme, mode, setTheme, toggleMode }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}
Por que un solo context y no dos
- Tema y modo siempre cambian juntos en la UI (el switch de tema muestra 4 combinaciones)
- Menos providers en el arbol: un solo
<ThemeProvider>vs<ThemeProv><ModeProv>...</ModeProv></ThemeProv> - Un solo hook:
const { theme, mode, setTheme, toggleMode } = useTheme()
Por que useCallback para los setters
Sin useCallback, cada render del provider crea nuevas funciones, lo que causa re-renders innecesarios en componentes que solo consumen setTheme o toggleMode. Con useCallback, la referencia es estable.
Coordinacion con el inline script
El ThemeProvider lee de localStorage en un useEffect, pero el inline script en <head> (ver TIL sobre dark mode flash) ya aplico las clases antes de React hydration. Asi no hay flash: el script pone las clases, React se monta, el provider lee el mismo localStorage y sincroniza estado.