Los componentes a menudo necesitan cambiar lo que aparece en la pantalla como resultado de una interacción.
Introducción
Crea un nuevo proyecto con el nombre react-state
:
> bun create vite react-state --template react-swc-ts
> cd react-state
> ...
Evento
React te permite agregar controladores de eventos a tu TSX.
A continuación tienes un botón que cuando haces clic en él no hace nada:
export default function App() {
return <button className="btn btn-primary m-5">No hago nada</button>
}
Un controlador de evento es una función que se ejecutará cuando el usuario haga algo:
export default function App() {
// Controlador de evento
function handleClick() {
alert("Bienvendio al Norte!")
}
return <button className="btn btn-primary m-5">No hago nada</button>
}
Puedes confirmar que el botón no hace nada y la función nunca se ejecuta 🥳
El botón tiene la propiedad "nullable" onClick?
, cuyo valor es una función que se ejecutará cuando el usuario haga clic en el botón:
export default function App() {
// Controlador de evento
function handleClick() {
alert("Bienvendio al Norte!")
}
return <button className="btn btn-primary m-5" onClick={handleClick}>No hago nada</button>
}
Ahora cuando haces clic en el botón, el navegador detecta este evento, y como la propiedad onClick
del botón no es nula, llama a la función correspondiente.
Por convención, es común llamar a los controladores de eventos como handle
seguido del nombre del evento.
A menudo verás onClick={handleClick}
, onMouseEnter={handleMouseEnter}
, etcétera.
Añade un controlador para onMouseEnter?
:
export default function App() {
// Controlador de evento
function handleClick() {
alert("Bienvenido al Norte!")
}
function handleOnMouseEnter() {
alert("Entrando en territorio botón")
}
return <button
className="btn btn-primary m-5"
onClick={handleClick}
onMouseEnter={handleOnMouseEnter}
>
No hago nada
</button>
}
También puedes pasar directamente una función en lugar de una referencia a una función:
export default function App() {
return <button
className="btn btn-primary m-5"
onClick={() => alert("Bienvenido al sur!")}
onMouseLeave={() => alert("Saliendo de territorio botón")}
>
Hago algo
</button>
}
A continuación tienes un texto editable:
export default function App() {
return <p
className="m-5 p-2 border"
contentEditable="true"
onKeyUp={() => alert("Por que tocas!")}
>
En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lantejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas, con sus pantuflos de lo mesmo, y los días de entresemana se honraba con su vellorí de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta, y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza, que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años; era de complexión recia, seco de carnes, enjuto de rostro, gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de Quijada, o Quesada, que en esto hay alguna diferencia en los autores que deste caso escriben; aunque por conjeturas verosímiles se deja entender que se llamaba Quijana. Pero esto importa poco a nuestro cuento: basta que en la narración dél no se salga un punto de la verdad.</p>
}
El texto es editable porqué el atributo contentEditable
del elemento <p>
és "true" (haz clic dentro y a escribir).
Añade algunas funciones más que respondan a eventos diferentes.
Si no sabes cuales, escribe on
y deja que el editor te sugiera las opciones disponibles.
Propiedades
Como los controladores de eventos son declarados dentro de un componente, tienen acceso a las props del componente.
Crea un componente MessageButton
:
export default function App() {
return <div>
<MessageButton message="Hola" />
<MessageButton message="Adiós" />
</div>
}
function MessageButton({ message }: { message: string }) {
return <button className="btn btn-primary m-5" onClick={() => alert(message)}>
Hago algo
</button>
}
Ahora tienes dos botones que muestran mensajes diferentes pero que tienen el mismo texto.
Modifica el código para pasar como prop el contenido del botón:
export default function App() {
return (
<div>
<MessageButton message="Hola">Bienvenido al club</MessageButton>
<MessageButton message="Adiós">
<img src="https://www.flaticon.com/download/icon/1998592?icon_id=1998592&author=219&team=219&pack=1998559&style=Lineal+Color&style_id=914&format=png&color=%23000000&colored=2&size=64&selection=1&type=standard" />
</MessageButton>
</div>
)
}
function MessageButton({ message, children }: { message: string, children: any }) {
return <button className="btn btn-primary m-5" onClick={() => alert(message)}>
{children}
</button>
}
Puedes ver que children
es una prop especial de tipo any
que se puede pasar como si escribieras HTML (es un objeto que contiene los elementos que envuelve un componente).
También puedes pasar como prop el controlador de evento:
export default function App() {
return (
<div>
<MessageButton onClick={() => alert("☀🌞🌊")}>
Playa
</MessageButton>
<MessageButton onClick={() => alert("☁⛅⛈🌤🌦🌧🌪🌫🌝")}>
Montaña
</MessageButton>
</div>
)
}
function MessageButton({ children, onClick }: { children: any, onClick: () => void }) {
return <button className="btn btn-primary m-5" onClick={onClick}>
{children}
</button>
}
Variables
Mostrar alertas no es la forma más eficiente de mostrar mensajes al usuario.
Es mejor utilizar una variable:
export default function App() {
let message = "Hola Mundo"
return (
<div className="m-5">
<p className="ms-5">{message}</p>
<MessageButton onClick={() => message = "☀🌞🌊"}>
Playa
</MessageButton>
<MessageButton onClick={() => message = "☁⛅⛈🌤🌦🌧🌪🌫🌝"}>
Montaña
</MessageButton>
</div>
)
}
function MessageButton({ children, onClick }: { children: any, onClick: () => void }) {
return <button className="btn btn-primary m-5" onClick={onClick}>
{children}
</button>
}
No te preocupes si el mensaje no cambia, el código funciona, pero React no reacciona al cambio del contenido de la variable message
.
Estado
Empezemos con un ejemplo senzillo:
export default function App() {
let message = "Hola Mundo"
return (
<div className="m-5">
<p>{message}</p>
<button className="btn btn-primary mt-3" onClick={() => message = "Ya sabes como funciona una variable de estado"}>
Mensaje
</button>
</div>
)
}
Como era de esperar React no reacciona al cambio del contenido de la variable message
.
React funciona de esta manera:
-
Renderiza la aplicación
-
Si se modifica una variable de estado, React reacciona y vuelve a renderizar los componentes afectados.
El Hook de useState
ofrece dos cosas:
- Una variable de estado para mantener los datos entre renderizados.
- Una función que setea el estado para actualizar la variable y provocar que React renderice el componente nuevamente.
Para agregar una variable de estado, debemos importar el useState
de React al inicio del archivo:
import { useState } from 'react';
Y ya puedes crear una variable con estado:
import { useState } from "react"
export default function App() {
const [message, setMessage] = useState("Hola Mundo")
return (
<div className="m-5">
<p>{message}</p>
<button className="btn btn-primary mt-3"
onClick={() => setMessage("Ya sabes como funciona una variable de estado")}>
Mensaje
</button>
</div>
)
}
El único argumento para useState
es el valor inicial de su variable de estado.
En este ejemplo, el valor inicial de message
se establece en "Hola Mundo"
.
Cada vez que el componente se renderiza, el useState
devuelve un array que contiene dos valores:
- La variable de estado (
message
) con el valor que almacenaste. - La función que establece el estado (
setMessage
) que puede actualizar la variable de estado y alertar a React para que renderice el componente nuevamente.
Modifica este código para utilizar una varialbe de estado:
export default function App() {
let message = "Hola Mundo"
return (
<div className="m-5">
<p className="ms-5">{message}</p>
<MessageButton onClick={() => message = "☀🌞🌊"}>
Playa
</MessageButton>
<MessageButton onClick={() => message = "☁⛅⛈🌤🌦🌧🌪🌫🌝"}>
Montaña
</MessageButton>
</div>
)
}
function MessageButton({ children, onClick }: { children: any, onClick: () => void }) {
return <button className="btn btn-primary m-5" onClick={onClick}>
{children}
</button>
}
import { useState } from "react"
export default function App() {
const [message, setMessage] = useState("Hola Mundo")
return (
<div className="m-5">
<p className="ms-5">{message}</p>
<MessageButton onClick={() => setMessage("☀🌞🌊")}>
Playa
</MessageButton>
<MessageButton onClick={() => setMessage("☁⛅⛈🌤🌦🌧🌪🌫🌝")}>
Montaña
</MessageButton>
</div>
)
}
function MessageButton({ children, onClick }: { children: any, onClick: () => void }) {
return <button className="btn btn-primary m-5" onClick={onClick}>
{children}
</button>
}
Múltiples variables de estado
Podemos tener más de una variable de estado de diferentes tipos en un componente.
A continuación tienes un código que utiliza 4 variable de estado:
import { useState } from "react"
export default function App() {
const [message, setMessage] = useState("Adivina un número del 0 al 9")
const [number] = useState(Math.floor(Math.random() * 9))
const [attemps, setAttemps] = useState(4)
const [guessed, setGuessed] = useState(false)
function attempted(event: React.KeyboardEvent<HTMLInputElement>) {
const guess = Number(event.key)
if (isNaN(guess)) {
setMessage("Sólo numeros")
} else if (guess == number) {
setGuessed(true)
setMessage("Adivinado 🤡")
}
else {
setAttemps(attemps - 1)
setMessage(`Te quedan ${attemps} intentos`)
}
}
return (
<div className="m-5">
<p>{message}</p>
{guessed || attemps == 0 ? <p>El número es {number}</p> : <input onKeyUp={(event) => attempted(event)}></input>}
</div>
)
}
Actividad. Añade un botón para resetear el juego.
Es una buena idea tener múltiples variables de estado si no se encuentran relacionadas entre sí, pero si encontramos que a menudo cambiamos dos variables de estado juntas, podría ser mejor combinarlas en una sola.
TODO Ejemplo
El estado es aislado y privado
El estado es local para una instancia de componente en la pantalla.
En otras palabras, si renderizas el mismo componente dos veces, cada copia tendrá un estado completamente aislado.
import { useState } from "react"
export default function App() {
return (
<div>
<Message text="☘ A la tercera va la vencida." />
<Message text="🌷 Al mal tiempo, buena cara." />
</div>
)
function Message({ text }: { text: string }) {
const [size, setSize] = useState(6)
function changeSize() {
if (size > 1)
setSize(size - 1)
}
return (
<div className="m-5">
<p className={`fs-${size}`}>{text}</p>
<button onClick={() => changeSize()}>
<span className="fs-1">+</span>
</button>
</div>
)
}
}
Puedes ver en nuestor ejemplo, que el componente Message
se renderiza dos veces y que son completamente independientes.
Actividad - Recetas
A continuación tienes el código inicial con una lista de comidas en las que sólo se muestra una comida.
type Meal = {
name: string
ingredients: string[]
image: string
}
const meals: Meal[] = [
{
"name": "Classic Margherita Pizza",
"ingredients": ["Pizza dough", "Tomato sauce", "Fresh mozzarella cheese", "Fresh basil leaves", "Olive oil", "Salt and pepper to taste"],
"image": "https://cdn.dummyjson.com/recipe-images/1.webp",
},
{
"name": "Vegetarian Stir-Fry",
"ingredients": ["Tofu, cubed", "Broccoli florets", "Carrots, sliced", "Bell peppers, sliced", "Soy sauce", "Ginger, minced", "Garlic, minced", "Sesame oil", "Cooked rice for serving"],
"image": "https://cdn.dummyjson.com/recipe-images/2.webp",
},
{
"name": "Chocolate Chip Cookies",
"ingredients": ["All-purpose flour", "Butter, softened", "Brown sugar", "White sugar", "Eggs", "Vanilla extract", "Baking soda", "Salt", "Chocolate chips"],
"image": "https://cdn.dummyjson.com/recipe-images/3.webp",
},
{
"name": "Chicken Alfredo Pasta",
"ingredients": ["Fettuccine pasta", "Chicken breast, sliced", "Heavy cream", "Parmesan cheese, grated", "Garlic, minced", "Butter", "Salt and pepper to taste", "Fresh parsley for garnish"],
"image": "https://cdn.dummyjson.com/recipe-images/4.webp",
},
{
"name": "Mango Salsa Chicken",
"ingredients": ["Chicken thighs", "Mango, diced", "Red onion, finely chopped", "Cilantro, chopped", "Lime juice", "Jalapeño, minced", "Salt and pepper to taste", "Cooked rice for serving"],
"image": "https://cdn.dummyjson.com/recipe-images/5.webp",
}
]
export default function App() {
const meal = meals[0]
return (
<div className="container m-5">
<h5>{meal.name}</h5>
<div className="row">
<div className="col-3"><img className="img-fluid" src={meal.image} /></div>
</div>
</div>
)
}
Añade un botón para mostrar los ingredientes que component la comida:
Añade dos botones: una para ir al siguiente plato y otro para ir al plato anterior.
Actividad - Netlifly
Crea una mini aplicación a tu elección con router y estado.
Despliega la aplicación en Netlifly