🌙 Implementando un Theme Provider en React
En React, uno de los patrones más comunes es el uso de un Theme Provider para gestionar el tema visual de la aplicación. Con esto podemos cambiar el aspecto de la interfaz de usuario según el tema (oscuro o claro) y permite que el usuario elija su preferencia de forma persistente.
En este post veremos cómo implementar un Theme Provider en React con TypeScript usando el contexto de React (context API
) y algunos estilos globales.
Crear la Aplicación de React
Para crear una aplicación de React configurada con TypeScript utilizando Vite, puedes usar el siguiente comando:
1
npm create vite@latest react-theme-provider -- --template react-ts
Una vez creado el proyecto, navegamos a la carpeta generada y ejecutamos el comando para instalar las dependencias:
1
2
cd react-theme-provider
npm install
Luego de forma opcional, inicia el servidor de desarrollo para ver la aplicación que nos crea vite:
1
npm run dev
Estructura de Archivos
A pesar de que vite nos crea la estructura y la configuración, recomiendo organizar los archivos de la carpeta src
de la siguiente manera:
1
2
3
4
5
6
7
8
9
10
src/
├── assets/
│ └── styles.css # Estilos globales para los temas
├── components/
│ └── ToggleTheme.tsx # Componente que cambia el tema
├── context/
│ └── ThemeContext.tsx # Contexto para gestionar el tema
├── App.tsx # Componente principal de la aplicación
├── index.tsx # Punto de entrada de la aplicación
└── index.css # Estilos globales para la aplicación
Creación del Contexto del Tema
Primero, vamos a crear el archivo ThemeContext.tsx
dentro de la carpeta context
. Este archivo se encargará de definir el contexto para el tema y la función para alternar entre temas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { createContext, useState, useContext, ReactNode } from "react";
type Theme = "light" | "dark";
// Definir el tipo de contexto
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// Crear el contexto con un valor por defecto
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Componente proveedor del tema
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>("light");
// Función para cambiar el tema
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Hooks personalizado para usar el contexto del tema
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme solo puede usarse dentro de ThemeProvider");
}
return context;
};
Resumiendo el código anterior, tenemos lo siguiente:
ThemeContext
: Este contexto gestiona el estado del tema (light
odark
)toggleTheme
: Esta función cambia el tema actual.useTheme
: Este hook personalizado nos permite acceder al contexto desde cualquier componente.
Crear el Componente de Cambio de Tema
Ahora vamos abrir o crear (si aún no lo haces) el componente ThemeToggle
que permitirá al usuario alternar entre el tema claro y el tema oscuro:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { useTheme } from "../context/ThemeContext";
const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Cambiar el tema a {theme === "light" ? "Oscuro" : "Claro"}
</button>
);
};
export default ThemeToggle;
Definir los estilos
Ahora vamos a aplicar los estilos de los temas usando variables CSS. En lugar de definir los colores en un objeto de JavaScript, utilizaremos las variables de CSS para cada tema. Luego, cambiamos las clases light
y dark
en el body
de la aplicación:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* Variables para el tema claro */
:root {
--background-color: #ffffff;
--text-color: #000000;
--button-bg: #007bff;
}
/* Variables para el tema oscuro */
body.dark {
--background-color: #333333;
--text-color: #ffffff;
--button-bg: #0056b3;
}
body {
margin: 0;
display: flex;
align-items: center;
min-height: 100vh;
justify-content: center;
font-family: sans-serif;
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
button {
background-color: var(--button-bg);
color: var(--text-color);
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
}
button:hover {
opacity: 0.8;
}
Uso del Contexto en los Componentes
Dentro de nuestro componente App.tsx
, vamos a usar el contexto para aplicar el tema y permitir que el usuario lo cambie con un botón:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from "react";
import ThemeToggle from "./components/ThemeToggle"
import { useTheme } from "./context/ThemeContext"
function App() {
const { theme } = useTheme();
useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<div>
<ThemeToggle />
<h1>Ejemplo de Theme Provider</h1>
</div>
);
};
export default App;
Renderizar la Aplicación en main.tsx
Finalmente, en main.tsx
, renderizamosla aplicación asegurándonos de que el ThemeProvider
esté envolviendo el componente App
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import './assets/styles.css';
import App from './App';
import { ThemeProvider } from './context/ThemeContext';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);
Persistir las Preferencia del usuario
Vamos a modificar el contexto del tema para usar localStorage
, y se recupere cuando el componente se monte.
Abrimos el archivo ThemeContext.tsx
y añadimos lo siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { createContext, useState, useContext, ReactNode } from "react";
type Theme = "light" | "dark";
// Definir el tipo de contexto
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
// Crear el contexto con un valor por defecto
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Componente proveedor del tema
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
// Obtener el tema guardado del localstorage, o por defecto 'light'
const storedTheme = localStorage.getItem('theme') as Theme | null;
const [theme, setTheme] = useState<Theme>(storedTheme || "light");
// Función para cambiar el tema
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Hooks personalizado para usar el contexto del tema
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme solo puede usarse dentro de ThemeProvider");
}
return context;
};
Y si observamos bien, solo hemos agregado 2 líneas:
const storedTheme = localStorage.getItem('theme') as Theme | null;
localStorage.setItem("theme", newTheme);
El resto se mantiene y ya tenemos guardada la preferencia del usuario en localStorage
.
Ejemplo en CodeSandbox
Puedes ver el ejemplo funcionando en el siguiente CodeSandBox:
Mi lema es