react

John Cardozo

John Cardozo

react

qué es react?

Librería para frontend

Soporta Javascript y Typescript

Sitios web & Interfaces de usuario

Estructura la capa de presentación

Componentes reutilizables

Virtual DOM

Desempeño y Testing

Muy popular en la industria

Single Page Applications

Por qué React?

componentes & datos

HTML

CSS

JS

State = Datos de la aplicación

Componentes reutilizables

Single

Page

Aplication

Global

Componente

jerarquia de componentes

Reutilización

Comunicación

Modularización

Eficiencia

Single

Page

Aplication

nodejs

Instalación

Verificación

node --version

npm --version

Node Package Manager

Node

inicio del proyecto

inicio del proyecto

configuración inicial del proyecto

Creación del proyecto usando Vite

npm init vite@latest nombre-proyecto -- --template react

Instalación de dependencias

cd nombre-proyecto
npm install

Ejecución del proyecto

npm run dev

estructura de archivos del proyecto

node_modules

folder de dependencias

public

archivos públicos

src

código fuente

assets

Archivos estáticos internos

*.css

Archivos de Estilo

*.jsx

Componentes React

index.html

Única página de la aplicación

package.json

Configuración de la app Nodejs

Main.jsx

Componente de Inicio

inicio de la app: main.jsX + index.html

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

index.html

Main.jsx

punto inicial de la aplicación

jsx

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
    </div>
  )
}

export default App

Importar assets

JSX

Importar estilos

className en vez de class

valores de State

Eventos

Exporta el componente

componente mínimo inicial

function App() {
  return (
    <div className="App">
      <h1>Tareas</h1>
    </div>
  );
}

export default App;

La función sólo puede retornar un elemento HTML

<div id="root">
  <div class="App">
      <h1>Tareas</h1>    
  </div>
</div>

React convierte JSX a HTML en el browser

El elemento raíz se puede reemplazar por:
<> </>

o

<Fragment>

</Fragment>

import { Fragment } from "react";

expresiones javascript en JSX

function App() {
  
  const nombre = "Cata";
  const mayorEdad = true;
  
  return (
    <div className="container">
      <h1>
        El estudiante {nombre} tiene {15 + 5} años
      </h1>
      <h2>{mayorEdad ? "Adulto" : "Niño"}</h2>
    </div>
  );
}

Declaración de variables

Uso de variables y expresiones

Expresión ternaria

componentes

componentes

componentes: funciones vs clases

const Header = () => {
   return (
      <div>
         <h1>Encabezado</h1>
      </div>
   )
}

export default header;

Función

import { Component } from "react";

export default class Header extends Component {
  render() {
    return (
      <div>
         <h1>Encabezado</h1>
      </div>
    )
  }
}

Clase

JSX

JavaScript Syntax Extension

Componentes

const Header = () => {
  return <div>Header</div>;
};

export default Header;

Componente básico: Header.jsx

ES7+ React/Redux/React-Native snippets

rafce

Genera un componente con función arrow basado en el nombre del archivo

import Header from "./components/Header";

function App() {
  return (
    <div>
      <Header />
    </div>
  );
}

export default App;

Importar: App.jsx

Componentes: propiedades

const Header = (props) => {
  return <div>{props.titulo}</div>;
};

Header.defaultProps = {
  titulo: "Admin de Tareas",
};

props = brinda acceso a las propiedades

import Header from "./components/Header";

function App() {
  return (
    <div>
      <Header titulo="Administrador" />
    </div>
  );
}

Utiliza los atributos para enviar datos al componente

Valor usado en la propiedad en caso de que el componente padre no brinde un valor

Las propiedades pueden ser leídas pero no modificadas

Componentes: propiedades - deconstrucción

const Header = ({ titulo, subtitulo }) => {
  return <div>
    <h1>{ titulo }</h1>
    <h2>{ subtitulo }</h2>
  </div>;
};

Header.defaultProps = {
  titulo: "Admin de Tareas",
  subtitulo: "2022",
};

En el componente hijo se usa { } para deconstruir el objeto y acceder a sus propiedades

import Header from "./components/Header";

function App() {
  return (
    <div>
      <Header titulo="Administrador" subtitulo="2022" />
    </div>
  );
}

Utiliza los atributos para enviar datos al componente

Componentes: tipos de propiedades

import Header from "./components/Header";

function App() {
  return (
    <div>
      <Header titulo={1} />
    </div>
  );
}

Componente Padre

import PropTypes from 'prop-types'

const Header = ({ titulo }) => {
  return <div>{titulo}</div>;
};

Header.propTypes = {
  titulo: PropTypes.string
  // Puede especificar si es obligatorio
  // titulo: PropTypes.string.isRequired
}

Definición de Tipos de Propiedades

impt

import PropTypes from "prop-types"

Enviar un entero genera un error en la consola dado que se espera un string

Propiedad desestructurada

Instalación de prop-types

npm i prop-types

Componentes: iterar una lista

const tareas = [
  { id: 1, titulo: "Correr" },
  { id: 2, titulo: "Programar" },
  { id: 3, titulo: "Ver TV" },
];

const Tareas = () => {
  return (
    <>
      {
        tareas.map((tarea) => (
        <h3 key={tarea.id}>{tarea.titulo}</h3>
        ))
      }
    </>
  );
};

export default Tareas;

Iterar una lista en el template

Función de listas: map

Each child in a list should have a unique "key" prop

key obligatorio

template: condicional

const tareas = [
  { id: 1, titulo: "Correr" },
  { id: 2, titulo: "Programar" },
  { id: 3, titulo: "Ver TV" },
];

const Tareas = () => {
  return (
    <>
      { tareas.length % 2 === 0 ? (
        <h2>Tareas pares</h2>
      ) : (
        <h2>Tareas impares</h2>
      ) }
      <hr>
      {tareas.map((tarea) => (
        <h3 key={tarea.id}>{tarea.titulo}</h3>
      ))}
    </>
  );
};

Operador ternario en el template

Expresión booleana

falso

verdadero

{ 
  tareas.length % 2 === 0 && 
    <h2>Tareas pares</h2>
}
<p>Welcome, { username || "Guest" }!</p>

&&

||

estilo de componentes

estilo de componentes

hoja de estilos global

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Main.jsx

La hoja global de estilos puede utilizarse en cualquier componente de la aplicación

estilo inline

const Header = ({ titulo }) => {
  return (
    <div>
      <h1 style={{ color: "red", backgroundColor: "yellow" }}>
        {titulo}
      </h1>
    </div>
  );
};

Estilo Inline

style = {  clase }

const Header = ({ titulo }) => {
  return (
    <div>
      <h1 style={estilosHeader}>{titulo}</h1>
    </div>
  );
};

const estilosHeader = {
  color: "white",
  backgroundColor: "green",
};

Estilo Inline + Clase separada

Sólo se admite camelCase para las propiedades CSS

style = { {  } }

Componente con estilos por parametro

import Button from "./Button";

const Header = ({ titulo }) => {
  return (
    <header>
      <h1 className="header">{titulo}</h1>
      <Button color="green" texto="Agregar" />
      <Button color="red" texto="Eliminar" />
    </header>
  );
};

Button.jsx

const Button = ({ color, texto }) => {
  return (
    <button style={{ backgroundColor: color }}>
      {texto}
    </button>
  );
};

export default Button;

Componente Padre: Header.jsx

vite + react + sCss

.form {
  display: flex;
  flex-direction: column;
  &__label {
    color: red;
  }
  &__input {
    color: magenta;
  }
  &__button {
    color: orange;
  }
}

Instalar SASS como dependencia de desarrollo

import "./Form.scss";

const Form = () => {
  return (
    <form className="form">
      <label className="form__label">New item</label>
      <input className="form__input" type="text" />
      <button className="form__button">Add</button>
    </form>
  );
};

export default Form;

Estilo Sass: Form.scss

npm install sass -D

Componente: Form.jsx

vite + react + sCss = use

@use "Variables.scss" as v;

.form {
  display: flex;
  flex-direction: column;
  border: 1px solid v.$main-color;
}

Form.scss

$main-color: lime;

Variables.scss

estado deL componente

estado del componente

estado: usestate

import { useState } from "react";

const Tareas = () => {
  
  const [tareas, setTareas] = useState([
    { id: 1, titulo: "Correr" },
    { id: 2, titulo: "Programar" },
    { id: 3, titulo: "Ver TV" },
  ]);
  
  return (
    <>
      {tareas.map((tarea) => (
        <h3 key={tarea.id}>{tarea.titulo}</h3>
      ))}
    </>
  );
};

export default Tareas;

Tareas.jsx

El arreglo tareas no se puede modificar directamente. Es inmutable!

Para hacer cambios se debe usar la función setTareas.

Estado

tareas

setTareas

variable

modificador

inicialización del estado

inicialización del estado

inicialización del estado

estado: usestate en app.jsx

import { useState } from "react";

import Header from "./components/Header";
import Tareas from "./components/Tareas";

function App() {
  const [tareas, setTareas] = useState([
    { id: 1, titulo: "Correr" },
    { id: 2, titulo: "Programar" },
    { id: 3, titulo: "Ver TV" },
  ]);

  return (
    <div className="container">
      <Header titulo="Admin" />
      <Tareas tareas={tareas} />
    </div>
  );
}

App.jsx

const Tareas = ({ tareas }) => {
  return (
    <ul>
      {tareas.map((tarea) => (
        <li key={tarea.id}>
          {tarea.titulo}
        </li>
      ))}
    <ul/>
  );
};

Tareas.jsx

componente unitario: tarea.jsx

const Tarea = ({ tarea }) => {
  return (
    <div className="task">
      <h3>{tarea.titulo}</h3>
    </div>
  );
};

export default Tarea;

Tarea.jsx

import Tarea from "./Tarea";

const Tareas = ({ tareas }) => {
  return (
    <>
      {tareas.map((tarea) => (
        <Tarea key={tarea.id} tarea={tarea} />
      ))}
    </>
  );
};

export default Tareas;

Tareas.jsx

iconos de react

import { FaTimes } from "react-icons/fa";

const Tarea = ({ tarea }) => {
  return (
    <div className="task">
      <h3>
        {tarea.titulo} <FaTimes style={{ color: "red" }} />
      </h3>
    </div>
  );
};

export default Tarea;

Uso del ícono

npm install react-icons

import { faTimes } from 'react-icons/fa'

Importar un ícono

Fontawesome

eventos

eventos

Eventos en un componente

Button.jsx

const Button = ({ color, texto }) => {
  
  // Función que se ejecuta al 
  // hacer click en el botón
  const clickHandler = () => {
    console.log("click me");
  };
  
  return (
    <button
      onClick={clickHandler}
      style={{ backgroundColor: color }}
    >
      {texto}
    </button>
  );
};

Al hacer click en el botón se ejecuta la función clickHandler

La función ejecutada puede opcionalmente recibir el evento

const clickHandler = (evento) => {
  console.log(evento);
};

escuchar el evento en componente padre

Button.jsx

const Button = ({ color, texto, onClick }) => {
  
  return (
    <button onClick={onClick}>{texto}</button>
  );
};

Button.propTypes = {
  texto: PropTypes.string,
  color: PropTypes.string,
  onClick: PropTypes.func,
};

Tipo de propiedad: func

Header.jsx

import Button from "./Button";

const Header = ({ titulo }) => {
  const clickHandler = () => {
    console.log("evento click recibido en el padre ");
  };
  return (
    <header>
      <h1 className="header">{titulo}</h1>
      <Button color="green" texto="Agregar" onClick={clickHandler} />
    </header>
  );
};

Paso de eventos entrE componentes

const borrarTarea = (id) => {
  setTareas(tareasActuales => {
    return tareasActuales.filter(tarea => tarea.id != id);
  });
};

return (
  <div className="container">
  <Header titulo="Admin" />
  <Tareas 
      tareas={tareas} 
      onDelete={borrarTarea} />
  </div>
);

App.jsx

const Tareas = ({ tareas, onDelete }) => {
  return (
    <>
      {tareas.map((tarea) => (
        <Tarea 
           key={tarea.id} 
           tarea={tarea} 
           onDelete={onDelete} />
      ))}
    </>
  );
};

Tareas.jsx

const Tarea = ({ tarea, onDelete }) => {
  return (
    <div className="task">
      <h3>
        {tarea.titulo}
        <FaTimes 
           onClick={() => onDelete(tarea.id)} />
      </h3>
    </div>
  );
};

Tarea.jsx

Eliminar tarea

evento doble click & cambiar estilo

const toggleTerminada = (id) => {
  setTareas(tareasActuales => {
    return tareasActuales.map(tarea => {
      tarea.id === id ? 
        { ...tarea, terminada: !tarea.terminada } : 
      tarea
    });
  });
}

return (
  <Tareas
    tareas={tareas}
    onToggle={toggleTerminada}
  />
);

App.jsx

const Tareas = ({ tareas, onToggle }) => {
  return (
    <>
      {tareas.map((tarea) => (
        <Tarea
          key={tarea.id}
          tarea={tarea}
          onToggle={onToggle}
        />
      ))}
    </>
  );
};

Tareas.jsx

const Tarea = ({ tarea, onToggle }) => {
  return (
    <div
      className={`task ${tarea.terminada ? "reminder" : ""}`}
      onDoubleClick={() => onToggle(tarea.id)}
    >
      <h3>
        {tarea.titulo}
      </h3>
    </div>
  );
};

Tarea.jsx

formularios

formularios

formulario: agregar tarea

AgregarTareaForm.jsx

import { useState } from "react";

const AgregarTareaForm = ({ onAgregar }) => {
  const [titulo, setTitulo] = useState("");
  const [terminada, setTerminada] = useState(false);

  const handleSubmit = (evento) => {
    evento.preventDefault();
    if (!titulo) {
      alert("Digite el titulo");
      return;
    }
    onAgregar({ titulo, terminada });
    setTitulo("");
    setTerminada(false);
  };
  return (
    <form className="add-form" onSubmit={handleSubmit}>
      <label>Tarea</label>
      <input type="text"
        value={titulo}
        onChange={(e) => setTitulo(e.target.value)}
      />
      <label>Terminada</label>
      <input type="checkbox"
        checked={terminada}
        value={terminada}
        onChange={(e) => {
          setTerminada(e.target.checked);
        }}
      />
      <input type="submit" 
        value="Agregar tarea" className="btn btn-block" />
    </form>
  );
};

Estado

Verificación

Invocar la función recibida

Inicializar el formulario

Modificar el estado

USO del formulario

import { useState } from "react";

import Tareas from "./components/Tareas";
import AgregarTareaForm from "./components/AgregarTareaForm";

function App() {
  const [tareas, setTareas] = useState([
    { id: 1, titulo: "Correr", terminada: true },
    { id: 2, titulo: "Programar", terminada: false },
    { id: 3, titulo: "Ver TV", terminada: true },
  ]);

  const agregarTarea = (tarea) => {
    const nuevaTarea = {
      id: crypto.randomUUID(),
      ...tarea,
    };
    setTareas([...tareas, nuevaTarea]);
  };

  return (
    <div className="container">
       <AgregarTareaForm onAgregar={agregarTarea} />     
       <Tareas tareas={tareas} />
    </div>
  );
}

App.jsx

Función que agrega el objeto a la lista

Modifica el State

Genera un valor aleatorio

peticiones rest api

peticiones rest api

axios: peticiones rest api

Instalación

npm install axios

[{id:1, title:"correr"}, {id:2, title:"leer"}]

Crear una tarea

Obtener todas las tareas

Modificar una tarea

Eliminar una tarea

POST /tareas

GET /tareas

PATCH /tareas/1

DELETE /tareas/1

{id:1, title:"correr"}

Obtener una tarea

GET /tareas/1

{title: "correr"}

{id:1, title:"correr"}

+

{title: "leer"}

+

{id:1, title:"leer"}

{id:1, title:"leer"}

get

get

post

patch

delete

backend simple: json-server

npm init -y
npm install json-server

Instalar json-server

{
  "tareas": [
    { "id": 1, "titulo": "tarea 1", },
    { "id": 2, "titulo": "tarea 2", },
    { "id": 3, "titulo": "tarea 3", },
  ]
}

db.json

"scripts": {
  "start": "json-server --watch db.json --host 127.0.0.1"
},

package.json

const promise = 
  fetch("http://localhost:3000/tareas")
    .then((res) => res.json())
    .then((data) => {
      data.forEach((t) => console.log(t.titulo));
    });

app.js

tarea 1
tarea 2
tarea 3

output

npm start

ejecución

BACKEND

FRONTEND

async/await: instrucciones asíncronas

// Hace la petición al Rest API
let respuesta = axios.get("http://localhost:3000/tareas");
// No muestra la respuesta del servidor
console.log(respuesta);

- Hace la petición al servidor

- Javascript no espera a que se obtenga respuesta

- Ejecuta la siguiente instrucción

Muestra null porque se ejecuta antes de que se obtenga la información del servidor

uso de async/await

const obtenerDatos = async () => {
  // Hace la petición al Rest API
  let respuesta = await axios.get("http://localhost:3000/tareas");
  // Muestra la respuesta obtenida del servidor
  console.log(respuesta);
}

Para usar await, la instrucción debe estar en un bloque async

Obtener datos - useeffect

import { useEffect } from "react";
import axios from "axios";

function App() {
  // State
  const [tareas, setTareas] = useState([]);

  // Se ejecuta al montar el componente
  useEffect(() => {
    // Función que obtiene las tareas
    const obtenerTareas = async () => {
      // Obtiene las tareas del backend
      const tareasBackend = await obtenerTareasBackend();

      // Modifica el State
      setTareas(tareasBackend);
    };
    // Invoca la función para obtener las tareas
    obtenerTareas();
  }, []);

  const obtenerTareasBackend = async () => {
    // Hace la petición al backend
    const respuesta = await axios.get("http://localhost:3000/tareas");
    // Retorna las tareas del backend
    return respuesta.data;
  };
  
}

GET al backend

agregar un dato

POST al backend

import axios from "axios";
function App() {
  const [tareas, setTareas] = useState([]);

  const agregarTarea = async (tarea) => {
 	// Obtiene las tareas del backend 
    const nuevaTarea = await axios.post(`http://localhost:3000/tareas`, tarea);

    // Agrega la tarea al arreglo
    setTareas([...tareas, nuevaTarea.data]);
  };
}

eliminar UN dato

import axios from "axios";

function App() {
  // State
  const [tareas, setTareas] = useState([]);

  const borrarTarea = async (id) => {
    try {
      // Elimina la tarea en backend
      await axios.delete(`http://localhost:3000/tareas/${id}`);

      // Modifica el State
      setTareas(tareas.filter((tarea) => tarea._id != id));
    } catch (error) {
      alert("Hubo un error");
    }
  };
  
}

DELETE al backend

react hooks

react hooks

react hooks

Funciones que permiten alterar o extender el comportamiento de los componentes

useState

useEffect

Extienden la funcionalidad de los métodos del ciclo de vida de los componentes

Permite almacenar, leer y modificar los datos locales de un componente. Los datos son el estado del componente.

useContext

Permite acceder a datos globales de aplicación desde cualquier componente sin tener que pasar datos mediante props

useReducer

Permite reducir la complejidad manteniendo la lógica en un solo lugar de fácil acceso

SOLO para componentes de función

Funciones JavaScript que permiten aislar partes reutilizables de un componente

reglas de los hooks

Los hooks solo pueden ser de uso en el nivel superior

Los hooks solo pueden ser utilizados en componentes de función

Un hook nunca debe de utilizarse dentro de ciclos, condicionales o funciones anidadas

Los hooks no pueden ser condicionales

Asegura que el resultado de la renderización del componente sea predecible

El orden del llamado de los hooks debe ser siempre el mismo

const Componente = () => {
  let variable = true;
  if (variable) {
    const [tareas, setTareas] = useState([]);
  }
}

No permitido

hook: usestate

import { useState } from "react";

const Titulo = () => {
  const [texto, setTexto] = useState("");
}

Importar el hook

Inicializar el hook

estado actual

función que actualiza el estado

Valor inicial

const Titulo = () => {
  const [texto, setTexto] = useState("");
  const handleClick = () => {
    setTexto("Hola");
  }
  return (
    <>
      <h1>{ texto }</h1>
      <button onClick={handleClick}>boton</button>
    </>
  )
}

Lee el estado

Modifica el estado

string number boolean array object

Permite mantener el estado de un componente

El estado son los datos del componente

hook: usestate - actualizar un objeto

const [tarea, setTarea] = useState({
  nombre: "Running",
  terminada: false
});

const updateTarea = () => {
  
  setTarea(tareaActual => {
    return { ...tareaActual, terminada: true }
  });
  
}

Estado actual

Actualiza el estado creando un nuevo objeto basado en las propiedades del objeto actual

Obtiene las propiedades del estado actual

Agrega las propiedades adicionales

hook: usestate - actualizar un arreglo

const [tareas, setTareas] = useState([]);

const agregarTarea = (nuevaTarea) => {
  
  setTareas([...tareas, nuevaTarea]);  
  
}

Obtiene los elementos del estado actual

Nuevo elemento

const eliminarTarea = (id) => {
  
  setTareas((tareasActuales) => {
    
    return tareasActuales.filter((tarea) => tarea.id !== id);
  
  });

};

Estado actual

Eliminar un objeto del arreglo

Agregar un objeto al arreglo

Retorna un nuevo arreglo filtrado

hook: useeffect

useEffect(() => {
  console. log ("Re-rendered");
});

Ejecutado cada vez que se actualiza el componente

useEffect(() => {
  console. log ("Componente creado!");
}, []);

Ejecutado SOLO la primera vez que se renderiza/monta el componente

useEffect(() => {
  console. log ("Componente creado!");
}, [titulo]);

Ejecutado  la primera vez y cuando cambia el valor de la dependencia

dependencias

sin dependencias

Sólo recibe la función por parámetro

useEffect recibe 2 parámetros: función y dependencias (opcional)

Extiende la funcionalidad de los métodos del ciclo de vida de un componente

hook: useeffect - ejemplo de dependencia

import { useState, useEffect } from "react";

const AgregarTareaForm = () => {
  const [titulo, setTitulo] = useState("");
  const [cantidad, setCantidad] = useState(0);

  useEffect(() => {
    setCantidad(titulo.length);
  }, [titulo]);

  return (
    <input
      type="text"
      id="titulo"
      value={titulo}
      onChange={
        (event) => setTitulo(event.target.value)
      }
    />
    <p>{cantidad}</p>
  );
};

useEffect se ejecuta al crear el componente y cuando cambia el título

Actualiza y muestra la cantidad de caracteres del título

AgregarTareaForm.jsx

hook: usecontext

Permite acceder a datos globales de aplicación desde cualquier componente sin tener que pasar datos mediante props

App

Header

Tareas

Tarea

Tarea

Prop drilling

Paso de valores por props

Context

App

Header

Tareas

Tarea

Tarea

Contexto

Acceso a los valores del Context

hook: usecontext - creación y configuración

import { createContext } from "react";

// Crea el contexto
const LocalContext = createContext({});

// Exporta el contexto
export default LocalContext;

context / LocalContext.js

// Contextos
import LocalContext from "./context/LocalContext";

// // Inicialización de contexto
const local = {
  es: {
    header: "Administrador de Tareas",
    name: "Tarea",
  },
  en: {
    header: "Task manager",
    name: "Task",
  },
};

function App() {

  return (
    <>
      <LocalContext.Provider value={local.en}>
	   ...
      </LocalContext.Provider>
    </>
  );
}

App.jsx

Creación del Contexto

Configuración del Contexto

Uso del contexto

hook: usecontext - uso

import { useContext } from "react";
import LocalContext from "../context/LocalContext";

const Header = () => {
  // Obtiene el contexto
  const local = useContext(LocalContext);

  return (
    <header>
      <h2>{local.header}</h2>
    </header>
  );
};

export default Header;

Header.jsx

Uso del Context en un componente hijo

Uso de datos del Context

Importar el Context

const Header = ({titulo}) => {
  return (
    <header>
      <h2>{titulo}</h2>
    </header>
  );
};

export default Header;

El componente ya no recibe el valor en la propiedad

hook: usecontext - actualizar el context - i

import PropTypes from "prop-types";

const SelectLanguage = ({ onLanguageChange }) => {
  return (
    <div className="languages">
      <span onClick={() => onLanguageChange("es")}>🇪🇸</span>
      <span onClick={() => onLanguageChange("en")}>🇬🇧</span>
    </div>
  );
};

SelectLanguage.propTypes = {
  onLanguageChange: PropTypes.func,
};

export default SelectLanguage;

SelectLanguage.jsx

Componente auxiliar que permite saber el nuevo valor que tendrá el Context

hook: usecontext - actualizar el context - iI

App.jsx

import LocalContext from "./context/LocalContext";

// Componentes propios
import Header from "./components/Header";
import SelectLanguage from "./components/SelectLanguage";

// Inicialización de contexto
const local = { ... };

function App() {
  const [language, setLanguage] = useState(local.es);

  const handleLanguageChange = (language) => {
    if (language === "es") {
      setLanguage(local.es);
    } else {
      setLanguage(local.en);
    }
  };

  return (
    <>
      <LocalContext.Provider value={language}>
        <SelectLanguage onLanguageChange={handleLanguageChange} />
        <Header />
          ...
      </LocalContext.Provider>
    </>
  );
}

export default App;

Valor inicial del Context

Cambio del Context usando el estado

actualizar context desde componente hijo

state

lang

setLang

App

Header

Tareas

Tarea

Tarea

Context

Se inicializa el value del Context con el State

El Componente modifica el state almacenado en el Context

actualizar el context desde componente hijo - i

App.jsx

import { createContext } from "react";
const LocalContext = createContext(null);
export default LocalContext;

LocalContext.js

const local = {
  es: {
    header: "Administrador de Tareas",
    name: "Tarea",
    add: "Agregar",
    init: "Limpiar",
    characters: "Caracteres",
    alt: "Eliminar tarea",
  },
  en: {
    header: "Task manager",
    name: "Task",
    add: "Add",
    init: "Reset",
    characters: "Characters",
    alt: "Remove task",
  },
};

export default local;

context / ContextData.js

import { useState } from "react";

import LocalContext from "./context/LocalContext";
import local from "./context/ContextData";

import Header from "./components/Header";
import SelectLanguage 
	from "./components/SelectLanguage";

function App() {
  const [language, setLanguage] = 
        useState(local.es);

  return (
    <>
      <LocalContext.Provider 
    	value={{ language, setLanguage }}>
        <SelectLanguage />
        <Header />
        ...
      </LocalContext.Provider>
    </>
  );
}

export default App;

Valor del Context: State

Inicialización del State

actualizar el context desde componente hijo - iI

SelectLanguage.jsx

import { useContext } from "react";
import LocalContext from "../context/LocalContext";
import local from "../context/ContextData";

const SelectLanguage = () => {
  let { setLanguage } = useContext(LocalContext);

  function handleChangeLanguage(lang) {
    setLanguage(lang === "es" ? local.es : local.en);
  }
  return (
    <div className="languages">
      <span onClick={() => handleChangeLanguage("es")}>🇪🇸</span>
      <span onClick={() => handleChangeLanguage("en")}>🇬🇧</span>
    </div>
  );
};

export default SelectLanguage;

Actualiza el valor del Context a través del set del State

const Header = () => {
  const { language } = useContext(LocalContext);

  return (
    <header>
      <h2>{language.header}</h2>
    </header>
  );
};

Uso del Context en componente hijo

hook: usereducer

Permite reducir la complejidad manteniendo la lógica en un solo lugar de fácil acceso

Cambiar la actualización del State por envío (dispatch) de acciones 

Crear una función reducer

Utilizar el reducer desde el componente

Los reducers son una alternativa a useState

Pasos para migrar useState a useReducer

1

2

3

event handler

Componente

Reducer

action

dispatch

state

Actualiza el estado

El Componente es renderizado

hook: usereducer - ESTADO INICIAL: USESTATE

function App() {
  const [tareas, setTareas] = useState([]);

  const agregarTarea = async (tarea) => {
    const nuevaTarea = await agregarTareaAPI(tarea);
    setTareas([...tareas, nuevaTarea]);
  };

  const toggleTerminada = (id) => {
    setTareas((tareasActuales) => {
      return tareasActuales.map((tarea) =>
         tarea.id === id ? { ...tarea, terminada: !tarea.terminada } : tarea
       );
    });
  };

  const eliminarTarea = (id) => {
    // tareasActuales representa el estado actual
    setTareas((tareasActuales) => {
      // Filtra las tareas sin la tarea con el id recibido
      return tareasActuales.filter((tarea) => tarea.id !== id);
    });
  };
}

App.jsx

Inicialmente, el estado es administrado desde el Componente con useState

hook: usereducer - CREACIÓN DEL REDUCER

const tareasReducer = (tareas, action) => {
  switch (action.type) {
    case "CARGAR":
      return action.tareas ? [...action.tareas] : [];
    case "AGREGAR":
      return [...tareas, action.nuevaTarea];
    case "MODIFICAR":
      return tareas.map((tarea) =>
        tarea.id === action.id
          ? { ...tarea, terminada: !tarea.terminada }
          : tarea
      );
    case "ELIMINAR":
      return tareas.filter((tarea) => tarea.id !== action.id);
    default:
      throw Error("Acción no soportada: " + action.type);
  }
};

export default tareasReducer;

TareasReducer.js

action.type = Acción a ejecutar en el reducer

action guarda los datos requerido para la lógica de negocio

El valor retornado en el reducer sobreescribirá el State

hook: usereducer - uso del reducer desde app

import { useEffect, useReducer } from "react";
import tareasReducer from "./reducers/TareasReducer";

function App() {
  const [tareas, dispatch] = useReducer(tareasReducer, []);

  useEffect(() => {
    const obtenerTareas = async () => {
      const tareas = await obtenerTareasAPI();
      if (tareas) {
        dispatch({ type: "CARGAR", tareas });
      } else {
        dispatch({ type: "CARGAR" });
      }
    };
    obtenerTareas();
  }, []);

  const agregarTarea = async (tarea) => {
    const nuevaTarea = await agregarTareaAPI(tarea);
    dispatch({ type: "AGREGAR", nuevaTarea });
  };

  const toggleTerminada = (id) => {
    dispatch({ type: "MODIFICAR", id });
  };

  const eliminarTarea = (id) => {
    dispatch({ type: "ELIMINAR", id });
  };  
}

App.jsx

Importa el reducer

Inicialización del reducer

dispatch

Recibe un objeto que contiene la acción sobre el estado y los parámetros requeridos para ejecutar la acción

Modifica el State

custom hooks

import { useState, useEffect, useContext } from "react";
import LocalContext from "../context/LocalContext";

const AgregarTareaForm = ({ onAddTask }) => {
  // Estado del formulario
  const [titulo, setTitulo] = useState("");
  const [descripcion, setDescripcion] = useState("");

  // Obtiene el contexto
  const { language } = useContext(LocalContext);

  return (
    <form onSubmit={handleSubmit} autoComplete="off">
      <fieldset>
        <label htmlFor="titulo">{language.title}: </label>
        <input
          type="text"
          id="titulo"
          value={titulo}
          onChange={(event) => setTitulo(event.target.value)}
        />
      </fieldset>
      <fieldset>
        <label htmlFor="descripcion">{language.description}: </label>
        <input
          type="text"
          id="descripcion"
          value={descripcion}
          onChange={(event) => setDescripcion(event.target.value)}
        />
      </fieldset>
    </form>
  );
};

Repetición de lógica

custom hooks: crear hook useinput - i

import { useState } from "react";

const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);

  const reset = () => {
    setValue(initialValue);
  };

  const bind = {
    value,
    onChange: (e) => setValue(e.target.value),
  };

  return [value, bind, reset];
};

export default useInput;

hooks / useInput.js

custom hooks: crear hook useinput - iI

components/AgregarTareaForm.jsx

import { useContext } from "react";
import useInput from "../hooks/useInput";
import LocalContext from "../context/LocalContext";

const AgregarTareaForm = ({ onAddTask }) => {
  const [titulo, bindTitulo, resetTitulo] = useInput("");
  const [descripcion, bindDescripcion, resetDescripcion] = useInput("");

  const handleSubmit = (event) => {
    event.preventDefault();

    const nuevaTarea = {
      titulo,
      descripcion,
      terminada: false,
    };
    onAddTask(nuevaTarea);
    resetTitulo("");
    resetDescripcion("");    
  };

  return (
    <form onSubmit={handleSubmit} autoComplete="off">
      <fieldset>
        <label htmlFor="titulo">{language.title}: </label>
        <input type="text" id="titulo" {...bindTitulo} />
      </fieldset>
      <fieldset>
        <label htmlFor="descripcion">{language.description}: </label>
        <input type="text" id="descripcion" {...bindDescripcion} />
      </fieldset>
    </form>
  );
};

Importa el hook

Uso del hook

Uso de funciones del hook

Uso del hook en el template

manejo de estado con redux

manejo de estado con redux

Redux Toolkit

redux

Ayuda a escribir aplicaciones con comportamiento consistente

Centraliza el estado y la lógica de la aplicación

Permite monitorear el estado de la aplicación con React DevTools

Funciona con cualquier framework/librería UI

Librería Javascript Open Source que permite administrar el estado centralizado de una aplicación

Datos de la aplicación

redux toolkit

Redux Toolkit es el conjunto oficial de herramientas para el manejo eficiente de Redux

Toolset recomendado para trabajar con Redux

Utiliza buenas prácticas en el manejo del estado

Store, reducers, slices (características)

No es de uso obligatorio pero sí recomendado

arquitectura de redux toolkit

Componente

Event Handler

dispatch

Reducer

State

UI

Store

1

2

3

4

5

1

El componente ejecuta un Event Handler (p.e. onClick)

2

El Event Handler ejecuta un dispatch a un Reducer

3

El dispatch ejecuta el Reducer del Store

4

El Reducer modifica (muta) la información del State

5

El State es actualizado y el cambio se refleja en el componente

redux toolkit - instalación

Instalación de Redux Toolkit

npm install @reduxjs/toolkit react-redux

Librerías instaladas - toolkit

redux

immer

redux-thunk

reselect

core, state management

permite mutar el state

maneja acciones async

simplifica los reducers

react-redux

Redux específico para React

Creación del proyecto usando Vite

npm init vite@latest nombre-proyecto -- --template react

ejemplo de trabajo: shopping cart

arquitectura del inicial

main.jsx

store.js

cartSlice.js

Crea el Slice y exporta el reducer

Importa el reducer y configura el State

Importa el Store y lo usa como Provider de la aplicación

creación del slice

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  cartItems: [],
  amount: 0,
  total: 0,
  isLoading: true,
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
});

export default cartSlice.reducer;

cartSlice

Slice = Característica

- actions
- caseReducers
- getInitialState
- name
- reducer

reducer

controla el State

exporta el reducer para su uso en el Store

configuración del store

store / index.js

import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./features/cart/cartSlice";

export const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});

main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import { store } from "./store";
import { Provider } from "react-redux";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

La aplicación está envuelta en el Provider de Redux

Utiliza el Reducer del Slice en el Store

extensión: redux devtools

Instalar extensión para Chrome/Edge

Log Monitor

Chart

componente accediendo al store: useselector

components / navbar / NavBar.jsx

import { GrCart } from "react-icons/gr";
import { useSelector } from "react-redux";

const NavBar = () => {
  const { amount } = useSelector((store) => store.cart);
  return (
    <header>
      <h1>My Store</h1>
      <div className="amount-container">
        <GrCart className="cart-icon" />
        <span className="badge">{amount}</span>
      </div>
    </header>
  );
};

export default NavBar;
@use "../variables" as v;
header {
  background-color: v.$header-bg-color;
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  h1 {
    color: v.$header-color;
    font-weight: 500;
  }
  .amount-container {
    .cart-icon {
      font-size: 2rem;
      path {
        stroke: v.$header-color;
      }
    }
    .badge {
      font-size: 1rem;
      background: v.$cart-badge;
      color: v.$header-color;
      padding: 0 5px;
      vertical-align: top;
      margin-left: -10px;
      // Circulo
      -webkit-border-radius: 9px;
      -moz-border-radius: 9px;
      border-radius: 9px;
    }
  }
}

navbar.scss

El hook useSelector permite acceder al Store

npm install react-icons

npm install sass -D

accediendo a datos locales

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";
import cardItems from "../../data/cartItems";

const initialState = {
  cartItems: cardItems,
  amount: 0,
  total: 0,
  isLoading: true,
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
});

export default cartSlice.reducer;
const cardItems = [
  {
    id: "1",
    title: "iPhone 14 Pro",
    price: 10,
    img: "https://fdn2.gsmarena.com/vv/bigpic/apple-iphone-14-pro-max-.jpg",
    amount: 1,
  },
  {
    id: "2",
    title: "Google Pixel 7 Pro",
    price: 20,
    img: "https://fdn2.gsmarena.com/vv/bigpic/google-pixel7-pro-new.jpg",
    amount: 1,
  },
  {
    id: "3",
    title: "Samsung Galaxy S23 Ultra",
    price: 30,
    img: "https://fdn2.gsmarena.com/vv/bigpic/samsung-galaxy-s23-ultra-5g.jpg",
    amount: 1,
  }
];

export default cardItems;

data / cartItems.js

Datos visibles en Redux DevTools

cart container: use selector

components / cart / CartContainer.jsx

Acceso al store

import { useSelector } from "react-redux";
import CartItem from "./CartItem";
import CartFooter from "./CartFooter";

const CartContainer = () => {
  const { cartItems, amount } = useSelector((store) => store.cart);

  return (
    <section className="cart-container">
      <header>
        <h2>Your Cart</h2>
      </header>
      { amount === 0 ? (
        <h4>No articles in your cart</h4>
      ) : (
        <div>
          {cartItems.map((item) => {
            return <CartItem key={item.id} {...item} />;
          })}
        </div>
        <CartFooter />
      ) }
    </section>
  );
};

export default CartContainer;

cart item: props

components / cart / CartItem.jsx

import { FaChevronUp, FaChevronDown } from "react-icons/fa";
import PropTypes from "prop-types";

const CartItem = ({ id, title, price, img, amount }) => {
  return (
    <article key={id} className="cart-item">
      <img src={img} alt={title} />
      <div>
        <h4>{title}</h4>
        <h4 className="item-price">${price}</h4>
        <button className="remove-btn">remove</button>
      </div>
      <div>
        <button className="amount-btn">
          <FaChevronUp />
        </button>
        <p className="amount">{amount}</p>
        <button className="amount-btn">
          <FaChevronDown />
        </button>
      </div>
    </article>
  );
};

CartItem.propTypes = {
  id: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  img: PropTypes.string.isRequired,
  amount: PropTypes.number.isRequired,
};

export default CartItem;

Instalación de prop-types

npm i prop-types -D

cart footer: use selector

components / cart / CartFooter.jsx

import { useSelector } from "react-redux";

const CartFooter = () => {
  const { total } = useSelector((store) => store.cart);

  return (
    <footer className="cart-footer">
      <hr />
      <div className="cart-total">
        <h4>
          Total <span>${total.toFixed(2)}</span>
        </h4>
      </div>
      <button className="btn-clear-cart">Clear cart</button>
    </footer>
  );
};

export default CartFooter;

Acceso al store

scss de componentes del cart

cart-container.scss

@use "../variables" as v;
@use "cart-item" as ci;
@use "cart-footer" as cf;

.cart-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  header {
    background-color: v.$cart-container-header;
    h2 {
      color: v.$header-color;
      font-weight: normal;
    }
  }
  
  @include ci.cart-item();
  @include cf.cart-footer();
}
@use "../variables" as v;

@mixin cart-item() {
  .cart-item {
    display: grid;
    align-items: center;
    grid-template-columns: auto 1fr auto;
    grid-column-gap: 1.5rem;
    margin: 1.5rem 2rem;
    img {
      height: 5rem;
    }
    h4 {
      margin-bottom: 0.5rem;
      font-weight: 500;
      letter-spacing: 0.5px;
    }
    .item-price {
      color: gray;
    }
    .remove-btn {
      color: v.$remove-btn-bg;
      letter-spacing: 2px;
      cursor: pointer;
      font-size: 0.85rem;
      background: transparent;
      border: none;
      margin-top: 0.375rem;
      &:hover {
        color: v.$remove-btn-bg-hover;
      }
    }
    .amount-btn {
      width: 24px;
      background: transparent;
      border: none;
      cursor: pointer;
      svg {
        color: v.$amount-btn-color;
      }
    }
    .amount {
      text-align: center;
      font-size: 1.25rem;
    }
  }
}

cart-item.scss

@use "../variables" as v;

@mixin cart-footer() {
  .cart-footer {
    display: flex;
    flex-direction: column;
    padding: 2rem;
    .cart-total {
      h4 {
        display: flex;
        justify-content: space-between;
        margin-top: 1.5rem;
      }
    }
    .btn-clear-cart {
      text-transform: uppercase;
      letter-spacing: 2px;
      background: transparent;
      padding: 0.8rem 1rem;
      margin: 2rem 10rem;
      color: v.$clear-cart-btn-color;
      border: 1px solid v.$clear-cart-btn-color;
      background-color: v.$clear-cart-btn-bg;
      border-radius: 5px;
      cursor: pointer;
    }
  }
}

cart-footer.scss

Incluye los scss de los componentes internos

$font-family: "Fira Sans", sans-serif;

// Colors
$header-bg-color: #0d63c5;
$header-color: #fff;
$cart-badge: #a9a9a9;
$cart-container-header: 
  mix(#fff, $header-bg-color, 30%);

$remove-btn-bg: #7170b1;
$remove-btn-bg-hover: #aa70b1;
$amount-btn-color: #5654b6;

$clear-cart-btn-color: #e700fc;
$clear-cart-btn-bg: 
  mix($clear-cart-btn-color, #fff, 10%);

variables.scss

variables.scss

creación de reducer: clear cart

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";
import cardItems from "../../data/cardItems";

const initialState = {
  cartItems: cardItems,
  amount: 4,
  total: 0,
  isLoading: true,
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    clearCart: (state) => {
      state.cartItems = [];
    },
  },
});

export const { clearCart } = cartSlice.actions;

export default cartSlice.reducer;

React Redux usa reducer que retornan el estado

immer permite modificar el State directamente sin retornarlo

Crea un nuevo action en el State

La creación del action evita el uso típico de action.type de Redux

modificar = mutar

Se agrega el action crearCart a la lista de actions del State

Exporta la acción para ser utilizada en los componentes

Si retorna un objeto, modificará TODO el State

reducers: {
  clearCart: (state) => {
    return {};
  },
},

uso de reducer: clear cart

components / cart / CartFooter.jsx

import { useSelector, useDispatch } from "react-redux";
import { clearCart } from "../../features/cart/cartSlice";

const CartFooter = () => {
  const dispatch = useDispatch();

  const { total } = useSelector((store) => store.cart);

  return (
    <footer className="cart-footer">
      <hr />
      <div className="cart-total">
        <h4>
          Total <span>${total.toFixed(2)}</span>
        </h4>
      </div>
      <button
        className="btn-clear-cart"
        onClick={() => {
          dispatch(clearCart());
        }}
      >
        Clear cart
      </button>
    </footer>
  );
};

export default CartFooter;

Importa y usa useDispatch

Usa dispatch para ejecutar el action

Muta el valor en el Store

Importa el reducer del Slice

creación de reducer: remove item

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";
import cardItems from "../../data/cardItems";

const initialState = {
  cartItems: cardItems,
  amount: 4,
  total: 0,
  isLoading: true,
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    clearCart: (state) => {
      state.cartItems = [];
    },
    removeItem: (state, action) => {
      const itemId = action.payload;
      state.cartItems = 
        state.cartItems.filter(
        (item) => item.id !== itemId);
    },
  },
});

export const { 
  clearCart, 
  removeItem } = cartSlice.actions;

export default cartSlice.reducer;

El action contiene datos enviados desde el dispatch: payload

Parámetro action

Exporta el action

uso de reducer: remove item

components / cart / CartItem.jsx

import { FaChevronUp, FaChevronDown } from "react-icons/fa";
import PropTypes from "prop-types";
import { removeItem } from "../../features/cart/cartSlice";
import { useDispatch } from "react-redux";

const CartItem = ({ id, title, price, img, amount }) => {
  const dispatch = useDispatch();
  return (
    <article key={id} className="cart-item">
      <img src={img} alt={title} />
      <div>
        <h4>{title}</h4>
        <h4 className="item-price">${price}</h4>
        <button
          className="remove-btn"
          onClick={() => {
            dispatch(removeItem(id));
          }}
        >
          remove
        </button>
      </div>
      <div>
        <button className="amount-btn">
          <FaChevronUp />
        </button>
        <p className="amount">{amount}</p>
        <button className="amount-btn">
          <FaChevronDown />
        </button>
      </div>
    </article>
  );
};

Importa y usa useDispatch

Usa dispatch para ejecutar el action

Envía el id del ítem el cual será recibido en action.payload del reducer

Importa el reducer del  Slice

creación de reducers: increase + decrease

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";
import cardItems from "../../data/cardItems";

const initialState = { ... };

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    ...
    increaseItemAmount: (state, { payload }) => {
      const cartItem = state.cartItems.find((item) => item.id === payload);
      cartItem.amount++;
    },
    decreaseItemAmount: (state, { payload }) => {
      const cartItem = state.cartItems.find((item) => item.id === payload);
      cartItem.amount--;
    },
  },
});

export const { clearCart, removeItem, 
              increaseItemAmount, 
              decreaseItemAmount 
             } = cartSlice.actions;

export default cartSlice.reducer;

El action desestructurado:

{ payload }

Parámetro action

Exporta los actions

uso de reducers: increase + decrease

components / cart / CartItem.jsx

import {
  removeItem,
  increaseItemAmount,
  decreaseItemAmount,
} from "../../features/cart/cartSlice";
import { useDispatch } from "react-redux";

const CartItem = ({ id, title, price, img, amount }) => {
  const dispatch = useDispatch();
  return (
    <article key={id} className="cart-item">
	...
    <div>
        <button
          className="amount-btn"
          onClick={() => dispatch(increaseItemAmount(id))}
        >
          <FaChevronUp />
        </button>
        <p className="amount">{amount}</p>
        <button
          className="amount-btn"
          onClick={() => dispatch(decreaseItemAmount(id))}
        >
          <FaChevronDown />
        </button>
      </div>
    </article>
  );
};

Importa y usa useDispatch

Usa dispatch para ejecutar el action

Envía el id del ítem el cual será recibido en action.payload del reducer

Importa los reducers del Slice

<button className="amount-btn" 
	onClick={() => {
      if (amount === 1) {
        dispatch(removeItem(id));
        return;
      }
      dispatch(decreaseItemAmount(id));
    }}
  >
    <FaChevronDown />
</button>

Si se reduce a un número negativo, se elimina el artículo

creación de reducer: calculate totals

features / cart / cartSlice.js

import { createSlice } from "@reduxjs/toolkit";
import cardItems from "../../data/cardItems";

const initialState = {
  cartItems: cardItems,
  amount: 4,
  total: 0
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    ...
    calculateTotals: (state) => {
      let amount = 0;
      let total = 0;
      state.cartItems.forEach((item) => {
        amount += item.amount;
        total += item.amount * item.price;
      });
      state.amount = amount;
      state.total = total;
    },
  },
});

export const {
  clearCart,
  removeItem,
  increaseItemAmount,
  decreaseItemAmount,
  calculateTotals,
} = cartSlice.actions;

Modifica el estado

Exporta el action

uso de reducer: calculate totals

App.jsx

import CartContainer from "./components/cart/CartContainer";
import NavBar from "./components/navbar/NavBar";
import { useDispatch, useSelector } from "react-redux";
import { calculateTotals } from "./features/cart/cartSlice";
import { useEffect } from "react";

const App = () => {
  const { cartItems } = useSelector((store) => store.cart);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(calculateTotals());
  }, [cartItems, dispatch]);

  return (
    <>
      <NavBar />
      <CartContainer />
    </>
  );
};

export default App;

Utiliza useEffect para monitorear los cambios en cartItems

Importa los hooks de react-redux

Importa el reducer del Slice

Utiliza dispatch para calcular los totales

Si en la lista de dependencias se agrega solo cartItems, es posible que se genere un warning

modal: confirmation clear cart

components / Modal.jsx

const Modal = () => {
  return (
    <aside className="modal-container">
      <div className="modal">
        <h4>
          Are you sure...?
        </h4>
        <div className="btn-container">
          <button type="button" 
                  className="btn confirm-btn">
            Confirm
          </button>
          <button type="button" 
                  className="btn cancel-btn">
            Cancel
          </button>
        </div>
      </div>
    </aside>
  );
};

export default Modal;
@use "../variables" as v;

.modal-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.7);
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  .modal {
    background: v.$modal-bg;
    width: 80vw;
    max-width: 400px;
    border-radius: 7px;
    padding: 2rem 1rem;
    text-align: center;
    h4 {
      line-height: 1.5;
      font-weight: 400;
      color: v.$modal-text;
    }
    .btn {
      text-transform: uppercase;
      letter-spacing: 2px;
      background: transparent;
      padding: 0.8rem 1rem;
      border-radius: 5px;
      cursor: pointer;
    }
    .btn-container {
      display: flex;
      justify-content: space-around;
      margin-top: 2rem;
      .confirm-btn {
        background-color: v.$modal-confirm-btn-bg;
        border: 1px solid v.$modal-confirm-btn-color;
        color: v.$modal-confirm-btn-color;
      }
      .cancel-btn {
        background-color: v.$modal-cancel-btn-bg;
        border: 1px solid v.$modal-cancel-btn-color;
        color: v.$modal-cancel-btn-color;
      }
    }
  }
}
$modal-text: #024c7a;
$modal-bg: #fff;
$modal-confirm-btn-color: #00bdfc;
$modal-confirm-btn-bg: mix($modal-confirm-btn-color, #fff, 10%);
$modal-cancel-btn-color: #e700fc;
$modal-cancel-btn-bg: mix($modal-cancel-btn-color, #fff, 10%);

variables.scss

components / modal.scss

modal: slices + reducer + store

features / modal / modalSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  isOpen: false,
};

const modalSlice = createSlice({
  name: "modal",
  initialState,
  reducers: {
    openModal: (state) => {
      state.isOpen = true;
    },
    closeModal: (state) => {
      state.isOpen = false;
    },
  },
});

export const { openModal, 
              closeModal } = modalSlice.actions;

export default modalSlice.reducer;

store / index.js

import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "../features/cart/cartSlice";
import modalReducer from "../features/modal/modalSlice";

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    modal: modalReducer,
  },
});

Agrega los nuevos reducers al Store

Se crea un nuevo feature en la aplicación a través del Slice

El Slice exporta los reducers para tener acceso al State

Los reducers son utilizados en el Store para que cualquier componente de la aplicación pueda tener acceso al State

Estado visible en Redux Toolkit

modal: usar el modal en la app

App.jsx

import CartContainer from "./components/cart/CartContainer";
import NavBar from "./components/navbar/NavBar";
import Modal from "./components/Modal";

import { useDispatch, useSelector } from "react-redux";
import { calculateTotals } from "./features/cart/cartSlice";
import { useEffect } from "react";

const App = () => {
  const { cartItems } = useSelector((store) => store.cart);
  const { isOpen } = useSelector((store) => store.modal);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(calculateTotals());
  }, [cartItems, dispatch]);

  return (
    <>
      {isOpen && <Modal />}
      <NavBar />
      <CartContainer />
    </>
  );
};

export default App;

Acceso al Store a través de useSelector

Dependiendo del State se muestra/oculta el modal

Si se cambia el initialState en modalSlice.jsx se muestra/oculta el componente Modal

modal: mostrar el modal desde cart footer

components / cart / CartFooter.jsx

import { useSelector, useDispatch } from "react-redux";
import { clearCart } from "../../features/cart/cartSlice";
import { openModal } from "../../features/modal/modalSlice";

const CartFooter = () => {
  const dispatch = useDispatch();
  const { total } = useSelector((store) => store.cart);

  return (
    <footer className="cart-footer">
      <hr />
      <div className="cart-total">
        <h4>
          Total <span>${total.toFixed(2)}</span>
        </h4>
      </div>
      <button
        className="btn-clear-cart"
        onClick={() => {
          dispatch(openModal());
        }}
      >
        Clear cart
      </button>
    </footer>
  );
};

Importa el reducer para abrir el Modal

Invoca el dispatch con openModal para mutar el State y mostrar el modal

Ya no se necesita clearCart porque no se limpiará el cart desde CartFooter sino desde el Modal

modal: clear cart + close modal

components / Modal.jsx

import { useDispatch } from "react-redux";
import { clearCart } from "../features/cart/cartSlice";
import { closeModal } from "../features/modal/modalSlice";

const Modal = () => {
  const dispatch = useDispatch();

  return (
    <aside className="modal-container">
      <div className="modal">
        <h4>
          Are you sure...?
        </h4>
        <div className="btn-container">
          <button
            type="button"
            className="btn confirm-btn"
            onClick={() => {
              dispatch(clearCart());
              dispatch(closeModal());
            }}
          >
            Confirm
          </button>
          <button
            type="button"
            className="btn cancel-btn"
            onClick={() => dispatch(closeModal())}
          >
            Cancel
          </button>
        </div>
      </div>
    </aside>
  );
};

Importa el reducer para cerrar el Modal

Cierra el modal usando el dispatch

Importa el reducer para limpiar el cart

Limpia el cart usando el dispatch

arquitectura de redux toolkit - async

Componente

Event Handler

dispatch

Reducer

State

UI

Store

1

2

3

4

5

1

El componente ejecuta un Event Handler (p.e. onClick)

2

El Event Handler ejecuta un dispatch a un Reducer

3

El dispatch ejecuta el Reducer del Store

4

El Reducer modifica (muta) la información del State

5

El State es actualizado y el cambio se refleja en el componente

API

Middleware

El middleware hace la petición al API obteniendo los datos

El middleware envía los datos al Reducer

6

6

7

7

operaciones async desde redux toolkit

features / cart / cartSlice.js

import axios from "axios";
import { createSlice, 
        createAsyncThunk } from "@reduxjs/toolkit";
const initialState = {
  cartItems: [],
  amount: 4,
  total: 0,
  isLoading: true,
};
const url = "http://localhost:3000/items";
export const getCartItems = createAsyncThunk(
        "cart/getCartItems", 
        async () => {
            try {
              const response = await axios.get(url);
              return response.data;
            } catch (error) { return []; }
        }
      );
const cartSlice = createSlice({
  ...
  extraReducers: (builder) => {
    builder
      .addCase(getCartItems.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getCartItems.fulfilled, (state, action) => {
        state.isLoading = false;
        state.cartItems = action.payload;
      })
      .addCase(getCartItems.rejected, (state) => {
        state.isLoading = false;
      });
  },
});

createAsyncThunk permite el manejo de operaciones asíncronas en Redux Tookit

uso de createAsyncThunk para peticiones asíncronas

extraReducers: manejo del ciclo de vida de la acción

pending

fulfilled

rejected

Informa a la aplicación si la información está siendo obtenida remotamente

Cada estado se maneja con un case del builder

Si hay error, retorna un arreglo vacío

uso de la operación asíncrona desde la app

App.jsx

import CartContainer from "./components/cart/CartContainer";
import NavBar from "./components/navbar/NavBar";
import Modal from "./components/Modal";

import { useDispatch, useSelector } from "react-redux";
import { 
  calculateTotals, 
  getCartItems } from "./features/cart/cartSlice";
import { useEffect } from "react";

const App = () => {
  const { cartItems, isLoading } = useSelector((store) => store.cart);
  const { isOpen } = useSelector((store) => store.modal);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(calculateTotals());
  }, [cartItems, dispatch]);

  useEffect(() => {
    dispatch(getCartItems());
  }, [dispatch]);

  if (isLoading) {
    return <h1>Loading...</h1>;
  }

  return (
    <>
      {isOpen && <Modal />}
      <NavBar />
      <CartContainer />
    </>
  );
};

uso del reducer asíncrona getCartItems

Importa el reducer asíncrono getCardItems

Uso de useSelector para tener acceso a la variable de carga

Se muestra un mensaje carga mientras se obtienen los datos asíncronos

manejo de error con thunk api

features / cart / cartSlice.js

import { openModal } from "../modal/modalSlice";

export const getCartItems = createAsyncThunk(
  "cart/getCartItems",
  async (_, thunkAPI) => {
    try {
      console.log(thunkAPI);
      console.log(thunkAPI.getState());
      thunkAPI.dispatch(openModal());
      const response = await axios.get(url);
      return response.data;
    } catch (error) {
      //return [];
      return thunkAPI.rejectWithValue("Something went wrong");
    }
  }
);
const cartSlice = createSlice({
  ...
  reducers: { ... },
  extraReducers: (builder) => {
    builder
      .addCase(getCartItems.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getCartItems.fulfilled, (state, action) => {
        state.isLoading = false;
        state.cartItems = action.payload;
      })
      .addCase(getCartItems.rejected, (state, payload) => {
        console.log(payload);
        state.isLoading = false;
      });
  },
});

Retorna un error que puede ser atrapado en el estado Rejected

Parámetro thunkAPI  permite tener acceso al State

Acceso al State

Acceso a otros Slices

componente loading...

components / Loading.jsx

import { dotWave } from "ldrs";
dotWave.register();

const Loading = () => {
  return (
    <div className="loading">
      <l-dot-wave size="80" color="#0d63c5" />
      <h2>Loading shopping cart...</h2>
    </div>
  );
};

export default Loading;

Librería Loaders

npm i ldrs

@use "../variables" as v;

.loading {
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  h2 {
    margin-top: 1rem;
    font-weight: 400;
    font-size: 1.3rem;
    color: v.$loading-text-color;
  }
}

components / loading.scss

import Loading from "./components/Loading";

const App = () => {
  ...
  
  if (isLoading) return <Loading />;

  return (
    <> ... </>
  );
};

App.jsx

Si carga muy rápido no se puede ver el componente

DevTools > Network > Throttling > slow 3G

react forms

react forms

Nativos & Formik

ejemplo: sign in + sign up + forgot password

ejemplo: sign in + sign up

state

step

setStep

App

SignIn

SignUp

SignContext

El value del context es el State

Cada componente modifica el state almacenado en el Context

Se crea un contexto para controlar el componente a visualizar y cada componente tiene acceso al contexto

inicio: app.jsx

App.jsx

import { useState } from "react";

// Components
import SignIn from "./components/SignIn";
import SignUp from "./components/SignUp";
import ForgotPassword from "./components/ForgotPassword";

function App() {
  const [step, setStep] = useState("signin");
  
  return (
    <>
        <div className="container">
          {step === "signin" && <SignIn />}
          {step === "signup" && <SignUp />}
          {step === "forgot" && <ForgotPassword />}
        </div>
    </>
  );
}

export default App;

inicio: signin.jsx

components / SignIn.jsx

const SignIn = () => {
  return (
    <main>
      <h3>Hello, friend!</h3>
      <div className="card">
        <form autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input type="email" id="email" autoFocus />
            <p className="error">Valid email required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="password">password</label>
            <input type="password" id="password" />
            <p className="error">Password should be 8 characters long (min, may, nums)</p>
          </fieldset>
          <button type="submit">login</button>
          <div className="forgot">
            Forgot password?
          </div>
        </form>
      </div>
      <p>Don&apos;t have an account?<span> Sign up</span>
      </p>
    </main>
  );
};

export default SignIn;

inicio: signup.jsx

components / SignIn.jsx

const SignUp = () => {
  return (
    <main>
      <h3>Welcome, join us!</h3>
      <div className="card">
        <form autoComplete="off">
          <fieldset>
            <label htmlFor="fullName">full name</label>
            <input type="text" id="fullName" autoFocus /> <p className="error">Required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="email">email</label>
            <input type="email" id="email" /> <p className="error">No valid email</p>
          </fieldset>
          <fieldset>
            <label htmlFor="password">password</label>
            <input type="password" id="password" /> <p className="error">Required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="confirm">Confirm password</label>
            <input type="password" id="confirm" /> <p className="error">Required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="framework">favorite framework</label>
            <select name="framework" id="framework">
              <option value="">Select your framework</option>
              <option value="react">React</option>
              <option value="vue">Vue</option>
              <option value="angular">Angular</option>
            </select> <p className="error">Required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="terms">
              <input type="checkbox" id="terms" /> Accept terms and conditions
            </label> <p className="error error-terms">Required</p>
          </fieldset>
          <button type="submit">register</button>
        </form>
      </div>
      <p>Already have an account? <span> Sign in</span></p>
    </main>
  );
};
export default SignUp;

inicio: forgot.jsx

components / ForgotPassword.jsx

const SignIn = () => {

  return (
    <main>
      <h3>Forgot password?</h3>
      <div className="card">
        <form autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input type="email" id="email" autoFocus />
            <p className="error">Valid email required</p>
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
      <p>
        Already have an account? <span>Sign in</span>
      </p>
    </main>
  );
};

export default SignIn;

estilo de formularios - I

components / _card.scss

@use "../variables" as v;
.card {
  background: v.$white;
  padding: 2rem;
  margin: 0 1.4rem;
  border-radius: 10px;
}

_layout.scss

@use "./variables" as v;
.container {
  width: 100vw;
  height: 100vh;
  padding: 20px;
  background-image: v.$container-bg;
  main {
    background-image: v.$main-bg;
    border-radius: 8px;
    width: 50%;
    max-width: 500px;
    margin: 0px auto;
    box-shadow: 0 7px 15px 0 rgba(0, 0, 0, 0.13),
      0 1px 4px 0 rgba(0, 0, 0, 0.11);
    h3 {
      font-family: v.$font-family;
      font-weight: 500;
      color: v.$white;
      text-align: center;
      letter-spacing: 0.1rem;
      padding: 1.8rem;
    }
    > p {
      font-family: v.$font-family;
      font-size: 1.2rem;
      font-weight: 300;
      color: v.$white;
      text-align: center;
      padding: 1rem;
      span {
        font-weight: 500;
        cursor: pointer;
      }
    }
  }
}

_variables.scss

// Colors
$white: #fff;
$gray: #9599a7;
$black: #696969;
$error: #e7496b;
// Backgrounds
$container-bg: linear-gradient(
  to right,
  #e85869,
  #dc4965,
  #d03a61,
  #c4285d,
  #b71259
);
$main-bg: linear-gradient(
  to left top,
  #c83373,
  #d83d70,
  #e7496b,
  #f45767,
  #ff6562
);
// Fonts
$font-family: "Montserrat", sans-serif;

npm install sass -D

estilo de formularios - II

components / _form.scss

@use "../variables" as v;
@use "./button" as b;
@use "./label" as l;
@use "./inputs" as i;
@use "./error" as e;

form {
  fieldset {
    border: none;
    display: flex;
    flex-direction: column;
    padding: 0.1rem 0;
    @include l.label();
    @include e.error();
    @include i.inputs();
  }
  @include b.button();
  .forgot {
    font-family: v.$font-family;
    color: v.$error;
    margin: 0 auto;
    text-align: center;
    margin-top: 1rem;
    cursor: pointer;
  }
}

components / _button.scss

@use "../variables" as v;
@mixin button() {
  button {
    background: transparent;
    background-image: v.$main-bg;
    color: v.$white;
    font-family: v.$font-family;
    font-size: 1rem;
    letter-spacing: 0.1rem;
    text-transform: uppercase;
    border: none;
    padding: 1.2rem 1rem;
    border-radius: 100px;
    width: 100%;
    margin-top: 1rem;
    cursor: pointer;
  }
}

components / _label.scss

@use "../variables" as v;
@mixin label() {
  label {
    font-family: v.$font-family;
    text-transform: uppercase;
    font-size: 0.8rem;
    font-weight: 500;
    letter-spacing: 0.05rem;
    color: v.$gray;
    margin-top: 10px;
  }
}

components / _error.scss

@use "../variables" as v;
@mixin error() {
  .error {
    color: v.$error;
    font-family: v.$font-family;
    font-size: 0.8rem;
    text-align: right;
    margin-top: 0.5rem;
    display: none;
    &-terms {
      text-align: left;
    }
  }
}

components / _inputs.scss

@use "../variables" as v;
@mixin inputs() {
  input[type="text"],
  input[type="email"],
  input[type="password"],
  select {
    font-family: v.$font-family;
    font-size: 1.2rem;
    border: 0;
    border-bottom: 1px solid v.$gray;
    outline: none;
    padding: 0.3rem 0;
    color: v.$black;
    &:invalid[data-focused="true"] ~ p {
      display: block;
    }
  }
}

Validación

resultado: sign in + sign up + forgot password

opcional: mostrar/ocultar password

styles / components / _input-pasword.scss

@use "../variables" as v;

.input-password {
  display: flex;
  align-items: center;
  
  input[type="text"],
  input[type="password"] {
    width: 100%;
  }
  .password-icon {
    cursor: pointer;
    color: v.$gray;
  }
}

src / components / InputPassword.jsx

import { useState } from "react";
import { AiOutlineEye, 
         AiOutlineEyeInvisible } from "react-icons/ai";

const InputPassword = () => {
  const [show, setShow] = useState(false);
  const [type, setType] = useState("password");

  const switchVisibility = (visibility) => {
    setShow(visibility);
    setType(visibility ? "text" : "password");
  };

  return (
    <div className="input-password">
      <input type={type} />
      {!show && (
        <AiOutlineEye
          className="password-icon"
          onClick={() => switchVisibility(true)}
        />
      )}
      {show && (
        <AiOutlineEyeInvisible
          className="password-icon"
          onClick={() => switchVisibility(false)}
        />
      )}
    </div>
  );
};

export default InputPassword;

Este componente puede ser utilizado en un componente que requiera mostrar/ocultar el password en un campo de un formulario

npm install react-icons

agregar context a la app

App.jsx

// Components
import { useState } from "react";
import SignIn from "./components/SignIn";
import SignUp from "./components/SignUp";

// Contexts
import SignContext from "./contexts/SignContext";

function App() {
  const [step, setStep] = useState("signin");
  return (
    <>
      <SignContext.Provider value={{ step, setStep }}>
        <div className="container">
          {step === "signin" && <SignIn />}
          {step === "signup" && <SignUp />}
        </div>
      </SignContext.Provider>
    </>
  );
}

export default App;

contexts / SignContext.js

import { createContext } from "react";
const SignContext = createContext();
export default SignContext;

Agrega el Context a la app

Usa el State en el Context para ser modificado en otros componentes

agregar context a los componentes hijos

components / SignIn.jsx

import { useContext } from "react";
import SignContext from "../contexts/SignContext";

const SignIn = () => {
  let { setStep } = useContext(SignContext);
  return (
    <main>
      <h3>Hello, friend!</h3>
      <div className="card">
        <form>
        ...
        </form>
      </div>
      <p>
        Don&apos;t have an account?{" "}
        <span
          onClick={() => {
            setStep("signup");
          }}
        >
          Sign up
        </span>
      </p>
    </main>
  );
};

components / SignUp.jsx

import { useContext } from "react";
import SignContext from "../contexts/SignContext";

const SignUp = () => {
  let { setStep } = useContext(SignContext);
  return (
    <main>
      <h3>Welcome, join us!</h3>
      <div className="card">
        <form>
        ...
        </form>
      </div>
      <p>
        Already have an account?
        <span
          onClick={() => {
            setStep("signin");
          }}
        >
          Sign in
        </span>
      </p>
    </main>
  );
};

Agrega el Context  al componente

Modifica el State a travé del value obtenido del Context

sign in: use state

components / SigIn.jsx

import { useContext, useState } from "react";
import SignContext from "../contexts/SignContext";

const SignIn = () => {
  let { setStep } = useContext(SignContext);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(email, password);
  };
  return (
    <main>
      <h3>Hello, friend!</h3>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email" autofocus
              value={email}
              onChange={(e) => {
                setEmail(e.target.value);
              }}
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="password">password</label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={(e) => {
                setPassword(e.target.value);
              }}
            />
            <p className="error">Password should be 8...</p>
          </fieldset>
        </form>
      </div>
    </main>
  );
};

Inicialización de useState

Modificación del estado con set

Asociación del State al elemento JSX

Inicialización de useState

sign in: atributos de validación

components / SigIn.jsx

const SignIn = () => {
  ...
  return (
    <main>
      <h3>Hello, friend!</h3>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input 
              type="email" id="email" autoFocus value={email} 
              required
              onChange={(e) => {
                setEmail(e.target.value);
              }}
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="password">password</label>
            <input type="password" id="password" value={password} 
              required
              pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
              onChange={(e) => {
                setPassword(e.target.value);
              }}
            />
            <p className="error">Password should be 8...</p>
          </fieldset>
          <button type="submit">login</button>
        </form>
      </div>
	  ...
    </main>
  );
};

Valor requerido, con longitud mínima  de 8 caracteres (minúsculas, mayúsculas, números)

Tipo email, requerido

sign in: evitar los mensajes de error al inicio

components / SigIn.jsx

const SignIn = () => {
  ...
  const [emailFocused, setEmailFocused] = useState(false);
  const [passwordFocused, setPasswordFocused] = useState(false);
  const handleEmailFocus = () => {
    setEmailFocused(true);
  };
  const handlePasswordFocus = () => {
    setPasswordFocused(true);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(email, password);
  };
  return (
    <main>
      <h3>Hello, friend!</h3>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input type="email" id="email" autoFocus value={email} required
              onChange={(e) => {
                setEmail(e.target.value);
              }}
              onBlur={handleEmailFocus}
              data-focused={emailFocused}
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <fieldset>
            <label htmlFor="password">password</label>
            <input type="password" id="password" value={password} required
              pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
              onChange={(e) => {
                setPassword(e.target.value);
              }}
              onBlur={handlePasswordFocus}
              data-focused={passwordFocused}
            />
            <p className="error">Password should be 8...</p>
          </fieldset>
          <button type="submit">login</button>
        </form>
      </div>
      ...
    </main>
  );
};

uso de onBlur, data-focused

Handle blur y focus

Cuando el componente es montado se ven los mensajes de error sin que el usuario haya interactuado con el formulario

components / _inputs.scss

@mixin inputs() {
  ...
  &:invalid[data-focused="true"] {
      border-bottom: 1px solid v.$error;
    }

  &:invalid[data-focused="true"] ~ p {
      display: block;
    }
  }
}

Se agrega onBlur y el focus de cada componente (onBlur = pierde el foco)

formik

npm install formik

Instalación

Librería para administración de formularios

Intuitiva

Declarativa

Adaptable

Hooks

Components

Estrategias

yup

Librería de validación

Opcional

formik: hooks - useformik

components / ForgotPassword.jsx

import { useFormik } from "formik";

const SignIn = () => {
  let { setStep } = useContext(SignContext);
  const formik = useFormik({
    initialValues: {
      email: "",
    },
  });

  return (
    <main>
      <h3>Forgot password?</h3>
      <div className="card">
        <form autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={formik.values.email}
              onChange={formik.handleChange}
              onBlur={formik.handleBlur}
              autoFocus
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
      <p> ... </p>
    </main>
  );
};

Importa Formik

Configura los valores del formulario

Establece:
value,

onChange,

onBlur

formik: desestructurar useformik

components / ForgotPassword.jsx

import { useFormik } from "formik";

const SignIn = () => {
  let { setStep } = useContext(SignContext);
  const { values, handleChange, handleBlur } = useFormik({
    initialValues: {
      email: "",
    },
  });

  return (
    <main>
      <h3>Forgot password?</h3>
      <div className="card">
        <form autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={values.email}
              onChange={handleChange}
              onBlur={handleBlur}
              autoFocus
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
      <p> ... </p>
    </main>
  );
};

Desestructura los valores del objeto formik

No utiliza el prefijo formik.

formik: validación con yup

schemas / forgotPasswordSchema.js

import * as Yup from "yup";

export const forgotPasswordSchema =
      Yup.object().shape({
        email: Yup
        .string()
        .email("Enter a valid email")
        .required("Required"),
});

Establece las condiciones y mensajes de validación de un campo

npm install yup

Instalación

Paquete de validación

Yup

components / ForgotPassword.jsx

import { forgotPasswordSchema } 
from "../schemas/forgotPasswordSchema";

const SignIn = () => {
  const { values, 
         handleChange, 
         handleBlur, 
         handleSubmit } = useFormik({
    initialValues: {
      email: "",
    },
    validationSchema: forgotPasswordSchema,
  });
  return (
    <main> ... </main>
  );
};

Uso de schema en la configuración

formik: mostrar errores con yup

Si hay un error de validación se almacena en errors

import { useContext } from "react";
import SignContext from "../contexts/SignContext";
import { useFormik } from "formik";
import { forgotPasswordSchema } from "../schemas/forgotPasswordSchema";
const onSubmit = () => {
  console.log("submitted");
};
const SignIn = () => {
  let { setStep } = useContext(SignContext);
  const { values, errors, handleChange, handleBlur, handleSubmit } = useFormik({
    initialValues: {
      email: "",
    },
    validationSchema: forgotPasswordSchema,
    onSubmit,
  });
  return (
    <main>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={values.email}
              onChange={handleChange}
              onBlur={handleBlur}
              autoFocus
              className={errors.email ? "error-forgot-input" : ""}
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
    </main>
  );
};

components / ForgotPassword.jsx

Si hay un error, agrega una clase CSS

components / _inputs.scss

@mixin inputs() {
  input[type="text"],
  input[type="email"],
  input[type="password"],
  select {
    ...
    &.error-forgot-input {
      border-bottom: 1px solid v.$error;
    }
  }
}

Clase CSS para marcar el error

A medida que se escribe se marca el error

onSubmit

formik: mostrar errores al usar el input

Si un elemento del formulario ha sido utilizado se agrega al objeto touched

const onSubmit = () => {
  console.log("submitted");
};

const SignIn = () => {
  const { values, errors, touched, handleChange, handleBlur, handleSubmit } =
    useFormik({
      initialValues: {
        email: "",
      },
      validationSchema: forgotPasswordSchema,
      onSubmit,
    });
  return (
    <main>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={values.email}
              onChange={handleChange}
              onBlur={handleBlur}
              autoFocus
              className={errors.email && touched.email ? "error-forgot-input" : ""}
            />
            <p className="error">Valid email required</p>
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
    </main>
  );
};

components / ForgotPassword.jsx

La clase CSS se agrega al elemento si: hay error y el elemento ha sido utilizado

formik: mostrar mensajes de error

const SignIn = () => {
  return (
    <main>
      <h3>Forgot password?</h3>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={values.email}
              onChange={handleChange}
              onBlur={handleBlur}
              autoFocus
              className={errors.email && touched.email ? "error-forgot" : ""}
            />
            {errors.email && touched.email && (
              <p className="error-message">{errors.email}</p>
            )}
          </fieldset>
          <button type="submit">Remember me!</button>
        </form>
      </div>
    </main>
  );
};

components / ForgotPassword.jsx

Muestra el mensaje de error con las mismas condiciones del input

components / _error.scss

@mixin error() {
  .error { ... }
  .error-message {
    color: v.$error;
    font-family: v.$font-family;
    font-size: 0.8rem;
    text-align: right;
    margin-top: 0.5rem;
  }
}

Agrega una clase CSS para los mensajes error

formik: enviar información on submit

const onSubmit = async (values, actions) => {
  console.log(values, actions);
  await new Promise((resolve) => setTimeout(resolve, 2000));
  actions.resetForm();
};
const SignIn = () => {
  let { setStep } = useContext(SignContext);
  const {
    values,
    errors,
    touched,
    handleChange,
    isSubmitting,
    handleBlur,
    handleSubmit,
  } = useFormik({
    initialValues: {
      email: "",
    },
    validationSchema: forgotPasswordSchema,
    onSubmit,
  });
  return (
    <main>
      <div className="card">
        <form onSubmit={handleSubmit} autoComplete="off">
          <fieldset>
            <label htmlFor="email">email</label>
            <input
              type="email"
              id="email"
              value={values.email}
              onChange={handleChange}
              onBlur={handleBlur}
              autoFocus
              className={errors.email && touched.email ? "error-forgot" : ""}
            />
            {errors.email && touched.email && (
              <p className="error-message">{errors.email}</p>
            )}
          </fieldset>
          <button disabled={isSubmitting} type="submit">
            Remember me!
          </button>
        </form>
      </div>
    </main>
  );
};

components / ForgotPassword.jsx

El botón se deshabilita mientras se procesa el formulario

components / _button.scss

@mixin button() {
  button {
    ...
    &:disabled {
      opacity: 0.35;
      cursor: default;
    }
  }
}

isSubmitting monitorea cuando el formulario se esté procesando

Re-inicializa el formulario

Simula el envío de una petición

formik: componente formik

import { Formik } from "formik";

const SignUp = () => {
  let { setStep } = useContext(SignContext);
  return (
    <main>
      <h3>Welcome, join us!</h3>
      <div className="card">
        <Formik
          initialValues={{
            fullName: "",
            email: "",
            password: "",
            confirm: "",
            framework: "",
            terms: false,
          }}
        >
          <form noValidate autoComplete="off">
            ...
          </form>
        </Formik>
      </div>
    </main>
  );
};

components / SignUp.jsx

Se usa el componente Formik alrededor de form

Se inicializan los valores del formulario como parámetro del componente

formik: componente form

import { Formik, Form } from "formik";

import { useContext } from "react";
import SignContext from "../contexts/SignContext";

const SignUp = () => {
  let { setStep } = useContext(SignContext);
  return (
    <main>
      <h3>Welcome, join us!</h3>
      <div className="card">
        <Formik
          initialValues={{
            fullName: "",
            email: "",
            password: "",
            confirm: "",
            framework: "",
            terms: false,
          }}
        >
          {(props) => {
            <Form>
              ...
            </Form>;
          }}
        </Formik>
      </div>
    </main>
  );
};

components / SignUp.jsx

En ver de usar <form> se usa el componente Form dentro de una función que tiene acceso a las propiedades

El componente Form necesita de componentes Field, los cuales se agregarán en el siguiente paso

formik: componente FIELD

import { Formik, Field, Form, ErrorMessage } from "formik";
const SignUp = () => {
  return (
    <main>
      <h3>Welcome, join us!</h3>
      <div className="card">
        <Formik
          initialValues={{
            fullName: "",
            email: "",
            password: "",
            confirm: "",
            framework: "",
            terms: false,
          }}
        >
          <Form>
            <fieldset>
              <label htmlFor="firstName">Full Name</label>
              <Field name="fullName" id="fullName" type="text" />
              <p className="error-message">Required</p>
            </fieldset>
            ...
          </Form>
        </Formik>
      </div>
      <p> Already have an account?
        <span onClick={() => { setStep("signin"); }}>Sign in</span>
      </p>
    </main>
  );
};

components / SignUp.jsx

En vez de usar <input> se usa el componente <Field>

Cada Field debe tener el atributo name para hacer match con los initialValues

Evita el uso explícito de onChange, onBlur, value, etc

formik: validacion con yup + errormessage

import { Formik, Field, Form, ErrorMessage } from "formik";
import { signUpSchema } from "../schemas/signUpSchema";
const initialValues = {
  fullName: "",
};
const onSubmit = (values, { setSubmitting }) => {
  setTimeout(() => {
    alert(JSON.stringify(values, null, 2));
    setSubmitting(false);
  }, 400);
};
const SignUp = () => {
  return (
    <main>
      <div className="card">
        <Formik
          initialValues={initialValues}
          validationSchema={signUpSchema}
          onSubmit={onSubmit}
        >
          <Form>
            <fieldset>
              <label htmlFor="fullName">Full Name</label>
              <Field name="fullName" id="fullName" type="text" />
              <ErrorMessage
                name="fullName"
                component="p"
                className="error-message"
              />
            </fieldset>
            <button type="submit">register</button>
          </Form>
        </Formik>
      </div>
    </main>
  );
};

components / SignUp.jsx

ErrorMessage referenciando el name

schemas / signUpSchema.js

import * as Yup from "yup";
export const signUpSchema = Yup
.object()
.shape({
  fullName: Yup
  .string()
  .required("Required"),
});

opcional: component y className

component: tag a ser renderizado. Por ejemplo: <p>

formik: sign up - yup scheme Completo

schemas / signUpSchema.js

import * as Yup from "yup";

export const signUpSchema = Yup.object().shape({
  fullName: Yup.string().required("Required"),
  email: Yup.string().email("Enter a valid email").required("Required"),
  password: Yup.string()
    .min(8, "Password must be at least 8 characters")
    .matches(
      /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/,
      "Password should be 8 characters long (lower, upper, nums)"
    )
    .required("Required"),
  confirm: Yup.string()
    .oneOf([Yup.ref("password"), null], "Passwords must match")
    .required("Required"),
  framework: Yup.string()
    .oneOf(["react", "vue", "angular"], "Must be a valid framework")
    .required("Required"),
  terms: Yup.bool()
    .oneOf([true], "You must accept the terms")
    .required("Required"),
});

formik: sign up - formulario completo

const initialValues = { fullName: "", email: "", password: "", confirm: "", framework: "", terms: false, };
const onSubmit = async (values, { setSubmitting }) => {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  alert(JSON.stringify(values, null, 2));
  setSubmitting(false);
};
const SignUp = () => {
  return (
    <Formik initialValues={initialValues} validationSchema={signUpSchema} onSubmit={onSubmit} >
      <Form autoComplete="off">
        <fieldset>
          <label htmlFor="fullName">Full Name</label>
          <Field name="fullName" id="fullName" type="text" />
          <ErrorMessage name="fullName" component="p" className="error-message" />
        </fieldset>
        <fieldset>
          <label htmlFor="email">Email</label>
          <Field name="email" id="email" type="email" />
          <ErrorMessage name="email" component="p" className="error-message" />
        </fieldset>
        <fieldset>
          <label htmlFor="password">Password</label>
          <Field name="password" id="password" type="password" />
          <ErrorMessage name="password" component="p" className="error-message" />
        </fieldset>
        <fieldset>
          <label htmlFor="confirm">Confirm password</label>
          <Field name="confirm" id="confirm" type="password" />
          <ErrorMessage name="confirm" component="p" className="error-message" />
        </fieldset>
        <fieldset>
          <label htmlFor="framework">favorite framework</label>
          <Field name="framework" id="framework" as="select">
            <option value="">Select your framework</option>
            <option value="react">React</option>
            <option value="vue">Vue</option>
            <option value="angular">Angular</option>
          </Field>
          <ErrorMessage name="framework" component="p" className="error-message" />
        </fieldset>
        <fieldset style={{ flexDirection: "row" }}>
          <Field type="checkbox" name="terms"></Field>
          <label htmlFor="terms">I agree to the terms</label>
          <ErrorMessage name="terms" component="p" className="error-message" />
        </fieldset>
        <button type="submit">register</button>
      </Form>
    </Formik>
  );
};

components / SignUp.jsx

patrones de manejo de estado y creación de componentes

patrones de manejo de estado y creación de componentes

patrones

Compound Component

Extensible Styles

Control Props

Stater Initializer

patrón compound component

Compound component es un patrón en el que los componentes se usan juntos de manera que comparten un estado implícito que les permite comunicarse entre sí en segundo plano

Los componentes trabajan juntos para tener un estado compartido y manejar la lógica juntos

Parent

Child

Child

Child

State

ejemplo de patrón compound component

TabSwitcher

Tab

TabPanel

TabContext

TabSwitcher expone su State a través de TabContext

Los componentes hijo Tab y TabPanel utilizan y modifican el State dependiendo de la operación requerida

Tab

TabPanel

TabSwitcher

TabContext

tabswitcher + tab + tabpanel

/* eslint-disable react/prop-types */
import { useState } from "react";
import TabContext from "./TabContext";

const TabSwitcher = ({ children }) => {
  const [activeTabId, setActiveTabId] = useState("1");
  return (
    <TabContext.Provider value={[activeTabId, setActiveTabId]}>
      {children}
    </TabContext.Provider>
  );
};

export default TabSwitcher;

components / TabSwitcher.jsx

import { useContext } from "react";
import TabContext from "./TabContext";

// eslint-disable-next-line react/prop-types
const Tab = ({ id, children }) => {
  const [, setActiveTab] = useContext(TabContext);

  return (
    <div onClick={() => { setActiveTab(id); }}>
      {children}
    </div>
  );
};

export default Tab;

components / Tab.jsx

import { useContext } from "react";
import TabContext from "./TabContext";

// eslint-disable-next-line react/prop-types
const TabPanel = ({ whenActive, children }) => {
  const [activeTabId] = useContext(TabContext);
  return (
    <div>
      {activeTabId === whenActive ? children : null}
    </div>
  );
};

export default TabPanel;

components / TabPanel.jsx

import { createContext } from "react";

const TabContext = createContext({});

export default TabContext;

context / TabContext.js

app using compound components

import TabSwitcher from "./tabs/Tabswitcher";
import Tab from "./tabs/Tab";
import TabPanel from "./tabs/TabPanel";

function App() {
  return (
    <TabSwitcher>
      <Tab id="1">
        <span>Tab 1</span>
      </Tab>
      <Tab id="2">
        <span>Tab 2</span>
      </Tab>
      <Tab id="3">
        <span>Tab 3</span>
      </Tab>
      <TabPanel whenActive="1">
        <div>Contenido 1</div>
      </TabPanel>
      <TabPanel whenActive="2">
        <div>Contenido 2</div>
      </TabPanel>
      <TabPanel whenActive="3">
        <div>Contenido 3</div>
      </TabPanel>
    </TabSwitcher>
  );
}

export default App;

App.jsx

agregar estilos a los componentes

<TabSwitcher>
  <div className="tab-container">
    <Tab id="1">
      <span>Tab 1</span>
    </Tab>
    <Tab id="2">
      <span>Tab 2</span>
    </Tab>
    <Tab id="3">
      <span>Tab 3</span>
    </Tab>
  </div>
  <TabPanel whenActive="1">
    <div className="tab-content">Contenido 1</div>
  </TabPanel>
  <TabPanel whenActive="2">
    <div className="tab-content">Contenido 2</div>
  </TabPanel>
  <TabPanel whenActive="3">
    <div className="tab-content">Contenido 3</div>
  </TabPanel>
</TabSwitcher>

App.jsx

$border-color: gray;

.tab-container {
  font-family: Arial, Helvetica, sans-serif;
  display: flex;
  gap: 0.2rem;
  .tab {
    border: 1px solid $border-color;
    border-top-left-radius: 5px;
    border-top-right-radius: 5px;
    padding: 0.2rem 0.4rem;
    background: lightgray;
    span {
      padding: 0.2rem 0.4rem;
      cursor: pointer;
    }
  }
}
.tab-content {
  font-family: Arial, Helvetica, sans-serif;
  margin-top: -1px;
  border: 1px solid $border-color;
  padding: 1rem;
}

styles / style.scss

<div className="tab"
    onClick={() => {
      setActiveTab(id);
    }} >
    {children}
</div>

components / Tab.jsx

<div className="tab-panel">
  {activeTabId === whenActive ? children : null}
</div>

components / TabPanel.jsx

generar para producción

generar para producción

GENERAR VERSIÓN PARA PRODUCCIÓN

npm run build

Comando para generar la aplicación

Proceso de generación

Resultado de generación

Versión mínima de HTML, CSS y JS

john cardozo

johncardozo@gmail.com

john cardozo

ReactJS

By John Cardozo

ReactJS

ReactJS

  • 256