Recientemente he estado migrando varios componentes que ya tenía a hooks, en el proceso, me encontré con un artículo que mostraba como añadir un tema oscuro a cualquier aplicación escrita en React. Asumí el reto de replicar su funcionalidad utilizando hooks y aquí esta.
Creando la aplicación
Puedes utilizar una aplicación ya existente o crear una nueva, en mi caso, cree una aplicación corriendo $ npx create-react-app dark-mode-react
.
Añadiendo temas CSS
Una vez tenía la aplicación creada, edite el archivo App.css
para añadir variables CSS que serán utilizadas por cada tema (dark/light).
/* Variables de color si el tema es nulo o light */
html {
--primary-color: #282c34;
--secondary-color: #fff;
}
/* Variables de color si el tema es dark */
html[data-theme="dark"] {
--primary-color: #fff;
--secondary-color: #282c34;
}
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: var(--secondary-color);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: var(--primary-color);
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
Los cambios fueron simples, un par de variables CSS y modifique las propiedades de background-color
y color
dentro de mi selector .App-header
para que hagan uso de estas variables.
Escribiendo la funcionalidad
En el componente App.js
, comencé por cambiar la clase por una función y añadir un checkbox.
import React from 'react';
import logo from './logo.svg';
import './App.css';
const App = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Click para cambiar el tema</p>
<label>
<input
type="checkbox"
onChange={() => ()}
/>
</label>
</header>
</div>
);
}
export default App;
A continuación, introduje los hooks useState
y useEffect
, ya que usamos el estado del checkbox para cambiar entre los temas light y dark.
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
const App = () => {
/**
* Determina si el checkbox debería estar checkeado basado en
* el contenido del localStorage
*/
const [checked, setChecked] = useState(localStorage.getItem("theme") === "dark" ? true : false);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Click para cambiar el tema</p>
<label>
<input
type="checkbox"
defaultChecked={checked}
onChange={() => ()}
/>
</label>
</header>
</div>
);
}
export default App;
useState
retorna una variable, en este caso checked, y otra función set
que se usa para actualizar el estado. En este caso, puedo utilizar la función setChecked
para actualizar el estado de checked
. useState
recibe como argumento el valor inicial de estado que queremos tener, por ejemplo const [number, setNumber] = useState(0)
haría que la variable number
se inicialice en 0
.
En nuestro ejemplo, estoy inicializando checked
a true
si ya existe un objeto en el localStorage
cuyo contenido sea dark
. Esto es con el fin de que cuando veamos el checkbox con valor true, checked o como lo quieras llamar, muestre el tema oscuro, y cuando sea false, unchecked, muestre el tema light.
Nota: Observa que al input se le ha añadido una nueva propiedad defaultChecked
que es igual al estado checked
.
Ahora, necesitamos que nuestro tema sea aplicado cuando nuestros componentes sean montados en la aplicación, para ello, usaremos useEffect
, el otro hook que importamos. Es parecido a componentDidMount()
y componentDidUpdate()
.
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
const App = () => {
/**
* Determina si el checkbox debería estar checkeado basado en
* el contenido del localStorage
*/
const [checked, setChecked] = useState(localStorage.getItem("theme") === "dark" ? true : false);
/**
* Cada vez que el estado checked cambie, actualiza la propiedad
* data-theme en el HTML para que use el tema que estamos almacenando
* en el localStorage
*/
useEffect(() => {
document
.getElementsByTagName("HTML")[0]
.setAttribute("data-theme", localStorage.getItem("theme"));
}, [checked]);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Click para cambiar el tema</p>
<label>
<input
type="checkbox"
defaultChecked={checked}
onChange={() => ()}
/>
</label>
</header>
</div>
);
}
export default App;
Finalmente, añadimos un handler para cuando se haga click en nuestro checkbox:
import React, { useEffect, useState } from "react";
import logo from "./logo.svg";
import "./App.css";
const App = () => {
/**
* Determina si el checkbox debería estar checkeado basado en
* el contenido del localStorage
*/
const [checked, setChecked] = useState(
localStorage.getItem("theme") === "dark" ? true : false
);
/**
* Cada vez que el estado checked cambie, actualiza la propiedad
* data-theme en el HTML para que use el tema que estamos almacenando
* en el localStorage
*/
useEffect(() => {
document
.getElementsByTagName("HTML")[0]
.setAttribute("data-theme", localStorage.getItem("theme"));
}, [checked]);
/**
* Actualiza el estado checked y el contenido de nuestro objeto
* theme en el localStorage basados en el checkbox
*/
const toggleThemeChange = () => {
if (checked === false) {
localStorage.setItem("theme", "dark");
setChecked(true);
} else {
localStorage.setItem("theme", "light");
setChecked(false);
}
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Click para cambiar el tema</p>
<label>
<input
type="checkbox"
defaultChecked={checked}
onChange={() => toggleThemeChange()}
/>
</label>
</header>
</div>
);
};
export default App;
Wohoo, eso es todo, ya puedes cambiar de tema en tu aplicación, refrescar la página y los cambios persistirán.
Puedes ver una comparación entre la implementación con un componente basado en React.Component
y mi implementación usando Hooks, haz click aquí.
Referencias
- https://dev.to/tesh254/how-to-add-a-dark-mode-to-your-react-web-app-4f1
- https://reactjs.org/docs/hooks-state.html
- https://reactjs.org/docs/hooks-effect.html