Usando Modo oscuro en tu aplicación de react! 🌙
Tags: spanish, react, webdev, javascript
Published: 2022-07-08 12:34:21

El modo oscuro es una de las funciones que se vería excelente implementado en tu aplicación, ya que mejoraría la experiencia de usuario dentro de tu aplicación.

Por lo que en esta ocasión, te enseñare como implementar el modo oscuro con React y sin alguna otra librería externa!

> 🚨 Nota: Este post requiere que sepas las bases de **React con TypeScript (hooks básicos)**.
>

Cualquier tipo de Feedback es bienvenido, gracias y espero disfrutes el articulo.🤗

&nbsp;

Tabla de contenido.
> 📌 [Tecnologías a utilizar.](#1)  
> 📌 [Creando el proyecto.](#2)   
> 📌 [Primeros pasos.](#3)   
> 📌 [Creando el componente Switch.](#4)   
> 📌 [Agregando unos cuantas tarjetas.](#5)   
> 📌 [Estilos para los temas.](#6)   
>> 📍 [Configurando las variables para el tema light.](#7)   
>> 📍 [Configurando las variables para el tema dark.](#8)   
>> 📍 [Usando las variables en nuestros estilo.](#9)   
>
> 📌 [Agregando la lógica para cambiar entre temas.](#10)   
>> 📍 [Controlando el estado del switch.](#11)   
>
> 📌 [Refactorizando la lógica en un custom hook.](#12)   
> 📌 [Conclusión.](#13)   
>> 📍 [Demostración en vivo.](#14)   
>> 📍 [Código fuente.](#15)   
>

&nbsp;


## 💡 Tecnologías a utilizar. <a id="1"></a>

- ▶️ React JS (version 18)
- ▶️ Vite JS
- ▶️ TypeScript
- ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post) 

&nbsp;


## 💡 Creando el proyecto. <a id="2"></a>

Al proyecto le colocaremos el nombre de: **`dark-light-app`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.  
Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.

```bash
cd dark-light-app
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

&nbsp;

## 💡 Primeros pasos. <a id="3"></a>

Ahora primero creamos una carpeta **`src/components`** y agregamos el archivo **Title.tsx** que contiene: 


```jsx
export const Title = () => {
    return (
        <h1>Dark - Light Mode </h1>
    )
}
```

Y ahora, dentro de la carpeta **`src/App.tsx`** borramos todo el contenido del archivo y colocamos el titulo que acabamos de crear.

```javascript
const App = () => {
  return (
    <div className="container">
      <Title />
    </div>
  )
}
export default App
```

Debería de verse así 👀:

![Title](https://res.cloudinary.com/dnxchppfm/image/upload/v1656862800/posts/dar-light/title_oljfns.png)

&nbsp;

## 💡 Creando el componente Switch. <a id="4"></a>

Ahora dentro de la carpeta **`src/components`** agregamos el archivo **Switch.tsx** y colocamos lo siguiente:

```js
export const Switch = () => {
    return (
        <div className="container-switch">
            <span>Change Theme </span>
            <label className="switch">
                <input type="checkbox" />
                <span className="slider"></span>
            </label>
        </div>
    )
}
```

Debería de verse así 👀:

![Switch](https://res.cloudinary.com/dnxchppfm/image/upload/v1656857751/posts/dar-light/switch_gmvybl.png)

&nbsp;

## 💡 Agregando unos cuantas tarjetas. <a id="5"></a>
 Nuevamente, dentro de la carpeta **`src/components`**
 agregamos el archivo **Card.tsx**.

 Primero crearemos el componente **Layout** que contendrá las tarjetas.

 ```js
export const LayoutCards = () => {
    return (
        <div className="grid-cards">
            <Card />
            <Card />
            <Card />
        </div>
    )
}
 ```
 Luego, el componente **Card** lucirá de esta manera:

 ```js
 export const Card = () => {
    return (
        <div className="card">
            <div className="card-image"></div>
            <h4 className="card-title">Lorem ipsum dolor sit.</h4>
            <p className="card-description">Lorem ipsum dolor sit amet consectetur adipisicing eli...</p>
            <div className="card-container-buttons">
                <button>Buy</button>
                <button>Show</button>
            </div>
        </div>
    )
}
 ```

Debería de verse algo como esto 👀:

![Cards](https://res.cloudinary.com/dnxchppfm/image/upload/v1656857751/posts/dar-light/cards_jdnojw.png)

&nbsp;

## 💡 Estilos para los temas. <a id="6"></a>
La idea es usar las variables con CSS para el tema dark y light.

&nbsp;

### 🟡 Configurando las variables para el tema light. <a id="7"></a>

Creamos una carpeta llamada **`src/styles`** y creamos el archivo **var.css**.
Este archivo se encargara de establecer las variables de CSS.

1- Para establecer las variables dentro de CSS usamos pseudo-clase **root** de la siguiente manera

```css
:root {
    
}
```
Dentro colocamos las variables que vamos a usar. Para definir variables utilizamos esta sintaxis
```css
--background: #f2f2f2;
``` 
Tenemos que colocar doble guion antes del nombre personalizado de nuestra propiedad, luego, colocamos dos puntos y agregamos el valor de dicha propiedad.
Aquí están las demás variables:
```css
:root {
    --background: #f2f2f2;
    --text-primary: #0f0f0f;
    --text-secondary: #4e4e4e;
    --accent: #dfb017;
    --accent-hover: #cea315; 
    --border: #1f1e1e;
    --shadow: 7px 15px 13px -4px #00000056;
}
```
Estas variables que acabamos de declarar sin para el **tema light** 

&nbsp;

### 🟡 Configurando las variables para el tema dark. <a id="8"></a>

Ahora vamos a definir los variables para el **tema oscuro**.  
Para ello, los nombres de las variables **tienen que ser nombradas exactamente igual que las variables anteriores** y solo cambiamos su valor después de los dos puntos.

```css
[data-theme='dark'] {
    --background: #05010a;
    --text-primary: #f2f2f2;
    --text-secondary: #a7a4a4;
    --accent: #6a5acd;
    --accent-hover: #5b4cbe; 
    --border: #696969;
    --shadow: 7px 15px 13px -4px #ffffff1b;
}
```

Nota que para las variables de tema oscuro, ya no usamos la pseudo-clase **root**, sino que hacemos referencia a un atributo personalizado que estamos definiendo como **theme**.

Este atributo personalizado, tiene que colocarse en una etiqueta HTML para que el modo oscuro funcione (No coloque el atributo manualmente, esto se hará de forma dinámica, usando react).  

Pero no en cualquier etiqueta, sino que, se debe colocar en la etiqueta de mayor jerarquía, como bor ejemplo el **body**.

Esto es un ejemplo de como debe verse
```html
<body data-theme='dark' >
<!-- content -->
<body>
```

Si colocamos el atributo **data-theme** en el otra etiqueta con menos jerarquía, solo el contenido de esa etiqueta usara el modo oscuro.  
**Por esta razón, hay que colocarla en la etiqueta con mayor jerarquía.**

```html
<body>
    <div data-theme='dark' >
        <!-- Dark theme -->
    </div>
    <div>
        <!-- Light theme -->
    </div>
<body>
```

&nbsp;


### 🟡 Usando las variables en nuestros estilos. <a id="9"></a>
Ahora, note que hemos creado un archivo **var.css** dentro de **`src/styles`**. Pero, ¿Donde las importamos?

Bueno, en mi caso me pareció, mejor importarlo en el archivo **src/index.css**.

Para importar archivos de **.css** en otro archivo **.css** usamos **@import url()** y agregamos la ruta donde se encuentra el archivo a importar.

**Esto de separar los archivos CSS es buena practica ya que ayuda a entender mejor el código de los estilos.**

Por cierto, debes colocar la importación en el top de tu archivo. 

```css
@import url('./styles/var.css');

body{
  font-family: 'Montserrat', sans-serif;
  font-weight: 600;
  transition: all .5s ease-in-out;
}
```

Bueno, ahora si, usemos las variables.  
Para usar las variables, se hace uso de la función **var()** y dentro le colocamos el nombre de la variable exactamente como la nombramos en nuestro archivo **var.css**

```css
body{
  background-color: var(--background);
  color: var(--text-primary);
}
```
Una vez que se hayan colocado las variables en los demás estilos (en las tarjetas, en el switch y el titulo), proseguiremos con agregar la lógica para cambiar entre temas. 

&nbsp;

## 💡 Agregando la lógica para cambiar entre temas. <a id="10"></a>

Primero, tenemos que controlar el estado el switch para poder obtener cuando esta 'on' / 'off' y dependiendo de esos valores usar un tema u otro.

&nbsp;

### 🟡 Controlando el estado del switch. <a id="11"></a>
1- Primero agregamos un estado. Este estado sera de tipo Theme, y solo aceptara el string 'dark' o 'light'.

```ts

type Theme = 'dark' | 'light'

export const Switch = () => {
    
    const [theme, setTheme] = useState<Theme>('light')

    return (
        <div className="container-switch">
            <span>Change Theme </span>
            <label className="switch">
                <input type="checkbox" />
                <span className="slider"></span>
            </label>
        </div>
    )
}
```

2- Creamos la función para controlar el evento del switch. 

El cual nombramos **handleChange**, recibe como parámetro el evento que emite por defecto el input. 
La función llama al setter **setTheme** y dentro hace una evaluación: 
- Si la propiedad checked del input esta en true, establece el tema 'dark'.

- Si la propiedad checked del input esta en false, establece el tema 'light'.

Ahora, la función **handleChange** se va a ejecutar cuando el input de tipo checkbox tenga un cambio y por eso se lo pasamos a método **onChange**.

Y la propiedad checked del mismo input,  le pasaremos una evaluación, ya que la propiedad checked solo acepta valores booleanos. La evaluación sera:

- Si el el valor del estado **theme** es 'dark', el valor de checked sera verdadero.

- Si el el valor del estado **theme** es 'light', el valor de checked sera falso.

 
```ts
type ChangeEvent = React.ChangeEvent<HTMLInputElement>

type Theme = 'dark' | 'light'

export const Switch = () => {
    
    const [theme, setTheme] = useState<Theme>('light')

    const handleChange = (e: ChangeEvent) => setTheme(e.target.checked ? 'dark' : 'light')

    return (
        <div className="container-switch">
            <span>Change Theme </span>
            <label className="switch">
                <input type="checkbox" onChange={handleChange} checked={theme === 'dark'} />
                <span className="slider"></span>
            </label>
        </div>
    )
}
```
3- Y ahora, recuerdan que íbamos a colocar el atributo personalizado **data-theme**, pues ahora toca hacerlo.  

Para ello usamos un efecto, el cual debe ejecutarse cada vez que el valor del estado **theme** cambie. Es por eso que lo colocamos en su arreglo de dependencias del **useEffect**.

Después, dentro del useEffect ejecutamos lo siguiente:
```js
document.body.setAttribute('data-theme', theme);
```
Básicamente, estamos accediendo a la etiqueta body (porque es el punto más alto que encierra toda nuestra aplicación), y le establecemos un nuevo atributo con la función **setAttribute**

- **setAttribute**, recibe en este caso dos parámetros:
    - el nombre del nuevo atributo.
    - el valor para ese nuevo atributo. 

Asi que, le establecemos el atributo **data-theme** con el valor del estado **theme**.

El código debería de verse como esto:
```ts
type ChangeEvent = React.ChangeEvent<HTMLInputElement>

type Theme = 'dark' | 'light'

export const Switch = () => {
    
    const [theme, setTheme] = useState<Theme>('light');

    const handleChange = (e: ChangeEvent) => setTheme(e.target.checked ? 'dark' : 'light');

    useEffect(() => {
        
        document.body.setAttribute('data-theme', theme);

    }, [theme]);

    return (
        <div className="container-switch">
            <span>Change Theme </span>
            <label className="switch">
                <input type="checkbox" onChange={handleChange} checked={theme === 'dark'} />
                <span className="slider"></span>
            </label>
        </div>
    )
}
```

Y listo, ya quedaría la funcionalidad para cambiar entre temas. 🥳

![switch theme](https://res.cloudinary.com/dnxchppfm/image/upload/v1656779026/posts/dar-light/dark-light_trvshy.gif)


Pero ahora, tenemos mucha lógica en nuestro archivo, por lo que toca, crear un custom hook! 👀

&nbsp;

## 💡 Refactorizando la lógica en un custom hook. <a id="12"></a>

Creamos una nueva carpeta dentro de **`src/hook`** creamos el archivo **useTheme.ts** y cortamos la lógica del archivo **Switch.tsx** y la pegamos en **useTheme.ts**.  
Hacemos las importaciones necesarias.

```ts
import { useEffect, useState } from 'react';

type ChangeEvent = React.ChangeEvent<HTMLInputElement>

type Theme = 'dark' | 'light'

export const useTheme = (): => {

    const [theme, setTheme] = useState<Theme>('light')

    const handleChange = (e: ChangeEvent) => setTheme(e.target.checked ? 'dark' : 'light')

    useEffect(() => {
        document.body.setAttribute('data-theme', theme);
    }, [theme])
}
```

Luego, este hook va a retornar un arreglo con dos elementos:
 - **theme**: el valor del estado del tema
 - **handleChange**: la función, que recibe un evento, para cambiar el estado entre temas y no retorna nada.


```ts
import { useEffect, useState } from 'react';

type ChangeEvent = React.ChangeEvent<HTMLInputElement>

type Theme = 'dark' | 'light'

type useThemeReturn = [ string, (e: ChangeEvent) => void ];

export const useTheme = (): useThemeReturn => {

    const [theme, setTheme] = useState<Theme>('light')

    const handleChange = (e: ChangeEvent) => setTheme(e.target.checked ? 'dark' : 'light')

    useEffect(() => {
        document.body.setAttribute('data-theme', theme);
    }, [theme])

    return [theme, handleChange]
}
```

Y también, vamos a recibir como parámetro el tema inicial y se lo agregamos al valor inicial del useState.

```ts
import { useEffect, useState } from 'react';

type ChangeEvent = React.ChangeEvent<HTMLInputElement>

type Theme = 'dark' | 'light'

type useThemeReturn = [ string, (e: ChangeEvent) => void ];

export const useTheme = (initialTheme:Theme): useThemeReturn => {

    const [theme, setTheme] = useState<Theme>(initialTheme)

    const handleChange = (e: ChangeEvent) => setTheme(e.target.checked ? 'dark' : 'light')

    useEffect(() => {
        document.body.setAttribute('data-theme', theme);
    }, [theme])

    return [theme, handleChange]
}
```

Ahora, toca llamar nuestro custom hook.
Devuelta en el archivo **src/components/Switch.tsx**

```ts
import { useTheme } from "../hook/useTheme";

export const Switch = () => {
    
    const [theme, handleChange] = useTheme('dark');

    return (
        <div className="container-switch">
            <span>Change Theme </span>
            <label className="switch">
                <input type="checkbox" onChange={handleChange} checked={theme === 'dark'} />
                <span className="slider"></span>
            </label>
        </div>
    )
}
```
**Y ahora definitivamente ya esta mas limpio y fácil de leer nuestro componente!** 🥳

&nbsp;


## 💡 Conclusión. <a id="13"></a>
Todo el proceso que acabo de mostrar, es una de las formas en que se puede hacer la funcionalidad para crear el modo oscuro y cambiar entre temas, sin usar alguna librería externa. 🌙

Espero haberte ayudado a entender como realizar esta funcionalidad y que logres aplicarla en tus futuros proyectos, muchas gracias por llegar hasta aquí! 🤗❤️  

Te invito a que comentes si es que conoces alguna otra forma distinta o mejor de como hacer esta funcionalidad. 🙌

&nbsp;

### 🟡 Demostración en vivo. <a id="14"></a>

[https://dark-light-theme-app.netlify.app](https://dark-light-theme-app.netlify.app/)

&nbsp;

### 🟡 Código fuente. <a id="15"></a>

{%embed https://github.com/Franklin361/dark-light-app %}