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.

reactjs hooks

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í.

Repositorio en GitHub.

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