John Cardozo
John Cardozo
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?
HTML
CSS
JS
State = Datos de la aplicación
Componentes reutilizables
Single
Page
Aplication
Global
Componente
Reutilización
Comunicación
Modularización
Eficiencia
Single
Page
Aplication
Instalación
Verificación
node --version
npm --version
Node Package Manager
Node
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
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
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
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
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";
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
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
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
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
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
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
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
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>
&&
||
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
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 = { { } }
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
.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
@use "Variables.scss" as v;
.form {
display: flex;
flex-direction: column;
border: 1px solid v.$main-color;
}
Form.scss
$main-color: lime;
Variables.scss
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
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
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
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
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);
};
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>
);
};
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
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
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
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
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
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
// 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
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
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]);
};
}
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Redux Toolkit
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 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
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
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
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
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
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
Instalar extensión para Chrome/Edge
Log Monitor
Chart
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
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
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;
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
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
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
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 {};
},
},
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Nativos & Formik
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
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;
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't have an account?<span> Sign up</span>
</p>
</main>
);
};
export default SignIn;
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;
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;
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
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
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
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
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'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
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
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
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)
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
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
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.
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
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
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
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
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
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
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
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
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>
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"),
});
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
Compound Component
Extensible Styles
Control Props
Stater Initializer
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
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
/* 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
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
<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
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
johncardozo@gmail.com