En la publicación pasada vimos cómo pasar información desde un componente padre a uno hijo.
Siguiendo esa línea, en esta ocasión vamos a ver cómo podemos hacer para renderizar la información cuando cambia mediante objetos y cómo enviar ese cambio desde un componente hijo a uno padre.
Para este ejemplo vamos a hacer lo siguiente:
Como podemos ver en el ejemplo, vamos a tener un botón para seleccionar y deseleccionar un ítem de una lista, y dependiendo de ambas posibilidades cambiará su color.
Por último, dependiendo de si el ítem esté seleccionado o no, cambiará el precio total.
Mutable vs Inmutable
Antes de comenzar con nuestro ejemplo principal, vamos a hacerlo con uno simple: un contador.
Los props son inmutables, esto quiere decir que al pasar información a un componente, ésta no podrá modificarse.
Para probar esto vamos a crear un componente: Contador.jsx:
export default function Contador( {numero} ) {
return (
<button type="button" className="primary"> Aumentar {numero} </button>
)
}
Lo agregamos a nuestro componente principal:
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import Contador from './components/Contador'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Contador numero={0} />
</React.StrictMode>,
)
Volviendo a nuestro componente Contador.jsx, el cual recibe como props un número, lo que queremos hacer es que al pulsar el botón se incremente ese número, para eso vamos a agregar un método:
const incrementar = () => {
numero++;
}
Y agregar al botón un evento click para ejecutar esta acción:
El código finalmente quedará así:
export default function Contador( {numero} ) {
const incrementar = () => {
numero++;
}
return (
<button onClick={incrementar} type="button" className="primary"> Aumentar {numero} </button>
)
}
Sin embargo, al pulsar en el botón, no pasa. Podemos verificar que el props realmente cambie:
const incrementar = () => {
numero++;
console.log(numero);
}
Al verificar la consola, podemos ver que el valor del props sí se está incrementando, pero siempre vemos el valor inicial, 0 (cero)
Para información mutable, debemos usar estados.
Estados
Un estado o useState() es un hook (más adelante veremos qué son los hooks) que nos devolverá un array, dividido en el valor del estado y una función que al ser llamada, recibirá como parámetro el valor nuevo del estado.
Debemos importarlo:
import { useState } from "react";
Ejemplo:
const [estado, setEstado] = useState(initialEstado);
Vamos a rehacer el componente:
import { useState } from "react";
export default function Contador( {initialNumero} ) {
const [numero, setNumero] = useState(initialNumero);
const incrementar = () => {
setNumero(numero + 1);
}
return (
<button onClick={incrementar} type="button" className="primary"> Aumentar {numero} </button>
)
}
Cosas que debemos tener en cuenta:
Al llamar a useState debemos pasarle como argumento el valor incial.
Puede ser un valor cualquier como:
const [numero, setNumero] = useState(0);
Pero también puede ser el valor pasado como props:
const [numero, setNumero] = useState(initialNumero);
En este caso, como nuestro estado se llamará «numero», vamos a tener que cambiarle el nombre a props
export default function Contador( {initialNumero} )
De lo contrario entrará en conflicto. Por eso también tendremos que cambiar la llamada en main.jsx:
<Contador initialNumero={0} />
La función useState, como dijimos antes nos devolverá un array con dos posiciones. También podemos hacer una desestructuración, como hicimos en la publicación pasada con un JSON:
const [numero, setNumero] = useState(initialNumero);
«numero» guardará la primera posición, que es el valor del estado; mientras que «setNumero» es la segunda posición, la función que modifica el valor de «numero».
Pero además esta función no sólo cambiará el valor del estado, sino que también le indicará al componente que debe renderizarse para mostrar los cambios realizados.
const incrementar = () => {
setNumero(numero + 1);
}
También podríamos probar un estado mediante un campo de ingreso:
import { useState } from "react";
export default function Contador( {initialNumero} ) {
const [numero, setNumero] = useState(initialNumero);
const incrementar = () => {
setNumero(numero + 1);
}
return (
<>
<input type="text" value={numero} onChange={(e) => setNumero(e.target.value)} />
<button onClick={incrementar} type="button" className="primary"> Aumentar {numero} </button>
</>
)
}
Como podemos ver:
<input type="text" value={numero} onChange={(e) => setNumero(e.target.value)} />
Mediante el evento onChange() cambiamos el valor del estado dependiendo de lo que haya ingresado el usuario por teclado.
Siguiente ejemplo
Para el siguiente ejemplo vamos a crear dos componentes MenuList.jsx y MenuItem.jsx.
El código de MenuList.jsx será el siguiente:
import { useState } from "react";
import MenuItem from "./MenuItem";
const data = [
{
nombre: 'Agua',
precio: 1000
},
{
nombre: 'Arroz',
precio: 1500
},
{
nombre: 'Mayonesa',
precio: 2000
}
];
export default function MenuList(){
const [total, setTotal] = useState(0);
return (
<table className="table">
<thead>
<tr>
<th style={{width: "40%"}}> Nombre </th>
<th style={{width: "40%"}}> Precio </th>
<th style={{width: "20%"}}> </th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colSpan={3}> Total: ${total} </td>
</tr>
</tfoot>
</table>
)
}
Importamos useState:
import { useState } from "react";
Y el componente que vamos a editar el código en unos instantes:
import MenuItem from "./MenuItem";
También tenemos un array con la información que queremos mostrar, la cual está hardcodeada, aunque en publicaciones futuras vamos a ver cómo extraerla de forma externa:
const data = [
{
nombre: 'Agua',
precio: 1000
},
{
nombre: 'Arroz',
precio: 1500
},
{
nombre: 'Mayonesa',
precio: 2000
}
];
Por último agregamos un estado:
const [total, setTotal] = useState(0);
Y lo renderizamos:
<tfoot>
<tr>
<td colSpan={3}> Total: ${total} </td>
</tr>
</tfoot>
Vamos a crear el componente el cual nos devolverá una fila de la tabla y a la que le tendremos que pasar los props:
MenuItem.jsx:
import { useState } from 'react';
export default function MenuItem( {nombre, precio} ){
const [selected, setSelected] = useState(false);
const seleccionar = () => {
setSelected(true);
}
const deSeleccionar = () => {
setSelected(false);
}
return (
<tr>
<td> {nombre} </td>
<td> ${precio} </td>
<td>
{
selected
?
<button onClick={deSeleccionar} type="button" className="btn btn-danger"> Deseleccionar </button>
:
<button onClick={seleccionar} type="button" className="btn btn-success"> Seleccionar </button>
}
</td>
</tr>
)
}
Como podemos ver tenemos un estado para saber si el usuario seleccionó o deseleccionó el ítem:
const [selected, setSelected] = useState(false);
Y dos funciones para cambiar el valor de este estado:
const seleccionar = () => {
setSelected(true);
}
const deSeleccionar = () => {
setSelected(false);
}
Operador ternario
Ya habíamos visto en una publicación pasada de Javascript el operador ternario. Este tipo de condicionales son muy utilizados en react cuando tenemos que condicionar lo que se renderiza:
{
selected
?
<button onClick={deSeleccionar} type="button" className="btn btn-danger"> Deseleccionar </button>
:
<button onClick={seleccionar} type="button" className="btn btn-success"> Seleccionar </button>
}
Es decir que si el ítem está seleccionado (selected true) vamos a ver el botón con la clase «btn btn-danger» (de color rojo) y la acción para cambiar el estado (selected a false)
Y si el ítem NO está seleccionado (selected false) vamos a ver el botón con la clase «btn btn-success» (de color verde) y la acción para cambiar el estado (selected a true)
.map()
Los arrays tienen un método llamado map() vamos a interar la información en el componente MenuList.jsx.
Dentro de:
<tbody>
</tbody>
Ingresamos:
{data.map( (item, index) => (
<MenuItem key={index} nombre={item.nombre} precio={item.precio} />
))}
Para entender lo que estamos haciendo, primero usamos el array data que creamos antes, el cual contenía una lista de objetos con este formato:
{
nombre: 'Agua',
precio: 1000
}
Después como podemos ver el método map() recibe un callback, el cual va a ejecutarse dependiendo de la cantidad de posiciones que tenga el array.
(item, index)
«item» es un alias, va a hacer referencia a cada una de las posiciones que estamos recorriendo (item.nombre, item.precio)
Mientras que «index» es el número de posición.
Como podemos observar en cada vuelta vamos a renderizar un componente con las props correspondientes:
{data.map( (item, index) => (
<MenuItem key={index} nombre={item.nombre} precio={item.precio} />
))}
Con respecto a:
key={index}
Esto no tiene que ver con las props del componente hijo, sino que es algo que necesita React para identificar cada uno de esos elementos (MenuItem), que entre otras cosas no debería repetirse (index no se repite, al ser tres posiciones del array son 0, 1, 2)
Por último vamos a agregar al componente principal index.jsx y ¡listo!
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import MenuList from './components/MenuList'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<div className="container">
<h1> Menú </h1>
<MenuList />
</div>
</React.StrictMode>,
)
Enviar información desde un componente hijo a un componente padre
Como podemos ver en el componente padre MenuList.jsx tenemos un estado el cual guarda el total de lo que seleccionó el usuario:
<tr>
<td colSpan={3}> Total: ${total} </td>
</tr>
Pero al seleccionar ítem desde el componente hijo, MenuItem.jsx, el total siempre es cero.
Hasta ahora vimos cómo enviar información desde un componente padre a un hijo, mediante props, pero no de forma inversa. La respuesta también son los props.
Vamos a cambiar los props de:
export default function MenuItem( {nombre, precio} )
Por:
export default function MenuItem( {nombre, precio, sumar, restar} )
sumar y restar, son funciones. Para llamarlas vamos a cambiar:
const seleccionar = () => {
setSelected(true);
}
const deSeleccionar = () => {
setSelected(false);
}
Por:
const seleccionar = () => {
setSelected(true);
sumar(precio);
}
const deSeleccionar = () => {
setSelected(false);
restar(precio);
}
Y ahora sólo resta agregar los props al componente MenuList.jsx.
Agregamos dos métodos para sumar y restar el total:
const sumarTotal = (precio) => {
setTotal(total + precio);
}
const restarTotal = (precio) => {
setTotal(total - precio);
}
Y luego cambiamos:
<tbody>
{data.map( (item, index) => (
<MenuItem key={index} nombre={item.nombre} precio={item.precio} />
))}
</tbody>
Por:
<tbody>
{data.map( (item, index) => (
<MenuItem key={index} nombre={item.nombre} precio={item.precio} sumar={sumarTotal} restar={restarTotal} />
))}
</tbody>
El código finalmente quedará:
MenuItem.jsx:
import { useState } from 'react';
export default function MenuItem( {nombre, precio, sumar, restar} ){
const [selected, setSelected] = useState(false);
const seleccionar = () => {
setSelected(true);
sumar(precio);
}
const deSeleccionar = () => {
setSelected(false);
restar(precio);
}
return (
<tr>
<td> {nombre} </td>
<td> ${precio} </td>
<td>
{
selected
?
<button onClick={deSeleccionar} type="button" className="btn btn-danger"> Deseleccionar </button>
:
<button onClick={seleccionar} type="button" className="btn btn-success"> Seleccionar </button>
}
</td>
</tr>
)
}
MenuList.jsx:
import { useState } from "react";
import MenuItem from "./MenuItem";
const data = [
{
nombre: 'Agua',
precio: 1000
},
{
nombre: 'Arroz',
precio: 1500
},
{
nombre: 'Mayonesa',
precio: 2000
}
];
export default function MenuList() {
const [total, setTotal] = useState(0);
const sumarTotal = (precio) => {
setTotal(total + precio);
}
const restarTotal = (precio) => {
setTotal(total - precio);
}
return (
<table className="table">
<thead>
<tr>
<th style={{ width: "40%" }}> Nombre </th>
<th style={{ width: "40%" }}> Precio </th>
<th style={{ width: "20%" }}> </th>
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<MenuItem key={index} nombre={item.nombre} precio={item.precio} sumar={sumarTotal} restar={restarTotal} />
))}
</tbody>
<tfoot>
<tr>
<td colSpan={3}> Total: ${total} </td>
</tr>
</tfoot>
</table>
)
}