Para poder realizar este taller ud debe:

  1. Tener instalado node.js en su máquina
  2. Haber realizado los codelabs de useState

El hook de efecto es el mecanismo con el que disponemos para sincronizar nuestros componentes con un sistema externo. Nos permite ejecutar procesos de manera automática cuando se finaliza el renderizado de un componente. Por ejemplo, el useEffect nos permitirá conectarnos a servicios externos, escuchar eventos globales del browser, iniciar una animación, controlar un modal o rastrear la visibilidad de un elemento en la vista.

Para crear un nuevo hook de efecto debemos agregar el llamado al método useEffect al inicio de la definición del componente. El llamado al método recibe 2 posibles parámetros: setup y dependencies. El parámetro setup hace referencia a la definición del efecto a realizar, normalmente este se ve representado como un llamado o la definición de una función. Por otro lado, el parámetro dependencies, es un arreglo compuesto de todos los valores "reactivos" que se usan dentro de la definición del efecto, veremos unos ejemplos para entender esta definición.

Como mencionamos en la sección anterior la creación de un hook de efecto está compuesta de un llamado al método useEffect junto con dos parametros:

useEffect(setup, dependencies?)

El parámetro setup será la definición de la lógica del efecto que se espera tener al realizar el renderizado de los elementos de la interfaz. Mientras que el parámetro dependencies hará referencia a la lista de parametros/dependencias que necesitaremos durante la ejecución del efecto. Veamos un ejemplo de un hook de efecto que se conecta a un servidor:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
 const [serverUrl, setServerUrl] = useState('https://localhost:1234');

 useEffect(() => {
   const connection = createConnection(serverUrl, roomId);
   connection.connect();
   return () => {
     connection.disconnect();
   };
 }, [serverUrl, roomId]);
 // ...
}

Analicemos este ejemplo antes de implementar nuestro propio hook de efecto. El parámetro setup, toma como valor una arrow function que se encarga de crear una conexión, para luego conectarse y dar como respuesta el método que desconecta al usuario al final. Esta sintaxis es un poco confusa, sin embargo el retorno del arrow function solo será utilizado cuando se realice un nuevo renderizado del componente o cuando se desmonte, por lo tanto, la respuesta de la arrow function que se define, es el mecanismo que le damos a React para garantizar la limpieza del estado y la conexión con los sistemas externos.

Ahora, el parámetro dependencies toma como valor un arreglo de dos valores, la URL del servidor y el id del canal del servidor al que nos queremos conectar. Estos valores que tenemos dentro del arreglo se caracterizan por ser valores "reactivos" dado que como se ve en la imagen de la interfaz, pueden ser cambiados por el usuario en cualquier momento.

Para la creación de nuestro primer hook de efecto empezaremos creando un nuevo proyecto como lo hizo en el tutorial de componentes. No se le olvide instalar los paquetes de bootstrap. Cree un componente llamado ImageViewer, dentro de ese componente ubique el siguiente contenido:

import { useEffect, useState } from "react";
import Card from "react-bootstrap/Card";
import Col from "react-bootstrap/Col";

function ImageViewer() {

   const [width, setWidth] = useState(100);
   const [height, setHeight] = useState(100);
   const [image, setImage] = useState("https://dummyimage.com/100x100.png")

   const handleWidthUpdate = (e) => {
       setWidth(e.target.value);
   }

   const handleHeightUpdate = (e) => {
       setHeight(e.target.value);
   }

   return (
     <Col>
     <Card style={{ width: "18rem" }}>
       <Card.Body className="mb-3">
         <Card.Title>Custom Size Image</Card.Title>
         <Card.Text><h2>Width:</h2><input value={width} onChange={handleWidthUpdate}></input></Card.Text>
         <Card.Text><h2>Height:</h2><input value={height} onChange={handleHeightUpdate}></input></Card.Text>
       </Card.Body>
     </Card>
     <img src={image}/>
   </Col>
   );
  }
 
 
export default ImageViewer;

Antes de ejecutar el proyecto, modifique el archivo index.js para que el renderizado haga referencia al nuevo componente que hemos creado, su código al interior del archivo index.js debería verse de la siguiente forma:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import ImageViewer from './components/ImageViewer';
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
 <Container className="mt-3">
 <Row>
   <ImageViewer />
 </Row>
 </Container>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Si inicia el proyecto verá que el proyecto se ve de la siguiente forma:

Nuestro objetivo será usar el useEffect para hacer que la imagen que se muestra en la parte inferior cumpla con los tamaños especificados en los inputs. Para esto, añadiremos en el componente ImageViewer un llamado al método useEffect, este llamado lo debemos hacer justo después de la definición de los estados.

Como se puede dar cuenta, el llamado que hemos hecho no tiene un valor para el parámetro dependencies, esto hará que el useEffect se llame cada vez que se genere un renderizado de la interfaz. Como la función que está siendo pasada por parámetro está vacía el useEffect no hará nada aún. Arreglemos esto mediante la modificación de la arrow function. Lo primero que haremos será obtener una nueva imagen haciendo uso de los valores width y height. Esto lo lograremos mediante el uso del método fetch. Su hook deberá verse de la siguiente forma:

useEffect(()=>{
       fetch("https://dummyimage.com/"+width+"x"+height+".png")
})

Si abre la consola de desarrollador del browser y se dirige a la sección de red, se dará cuenta que cada vez que modifique los valores en los inputs se realizará el fetch de la imagen desde la URL. Continuaremos nuestro ejercicio mediante la asignación de la imagen obtenida al elemento tipo img que tenemos en la interfaz. Esto lo lograremos mediante el uso de cláusulas then a continuación del fetch. Modifique su hook para que se vea de la siguiente forma:

useEffect(()=>{
       fetch("https://dummyimage.com/"+width+"x"+height+".png")
       .then(data => data.blob())
       .then(blob => {setImage(URL.createObjectURL(blob))})
})

Ahora si vuelve a ver su consola de desarrollador del browser, podrá ver que se están haciendo peticiones constantemente para obtener la imagen, esto sucede dado que React está constantemente renderizando los cambios de la interfaz, cómo no hemos definido las dependencias "reactivas" del hook se seguirán haciendo bastantes peticiones. para esto agregaremos como segunda parámetro al hook, un arreglo que contenga nuestro dos variables, width y height.

Ahora cada vez que usted cambia el valor en cualquiera de esos dos valores el useEfect se llamará y se realizará la función definida en el setup.

Podemos definir a un hook de efecto como una función que se ejecuta una vez que un componente se ha renderizado.

Cree un nuevo componente que se llame mascotas.js y modifique el index.js para que renderice un componente de este tipo.

Este es el contenido del archivo mascotas.js:

import Mascota from "./mascota";
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

const { useEffect, useState } = require("react");

function Mascotas () {

   const [mascotas, setMascotas] = useState([]);
   useEffect(()=>{
       const URL = "https://gist.githubusercontent.com/josejbocanegra/829a853c6c68880477697acd0490cecc/raw/99c31372b4d419a855e53f0e891246f313a71b20/mascotas.json";
       fetch(URL).then(data => data.json()).then(data => {
           setMascotas(data);
       })
   }, []);

   return(
       <div className="container">
           <h1>Listado de mascotas</h1>
           <hr></hr>
           <Row>
               {mascotas.map(mascota => <Col><Mascota mascota={mascota} key={mascota.id}/></Col>)}               
           </Row>
          
       </div>
   )
}

export default Mascotas;

Primero tenemos un hook de estado para almacenar el listado de mascotas. Este listado lo vamos a obtener desde un gist de GitHub. Luego hacemos el llamado al hook de efecto. En este definimos la url del gist y con ayuda de fetch haremos una petición a esa url.

Recordemos que fetch es una interfaz de JavaScript para acceder y manipular partes del canal HTTP tales como peticiones y respuestas.

Cuando los datos estén listos, es decir cuando la promesa se haya resuelto seteamos la variable mascotas usando el método setMascotas.

En el cuerpo del componente hemos definido un contenedor y un título de primer nivel. Luego iteramos sobre el arreglo mascotas para crear un componente de tipo Mascota por cada objeto de ese arreglo. Inicialmente el arreglo estará vacío.

Cuando el componente se renderiza se ejecutará el hook de efecto el cual actualiza el estado del componente lo que hará que el componente se renderice nuevamente. Para evitar que la aplicación quede en un ciclo infinito, hook de efecto debe recibir como segundo parámetro un arreglo vacío.

Ahora veamos el contenido del componente Mascota (archivo src/componentes/mascota.js):

import Card from 'react-bootstrap/Card';

function Mascota(props){
   return(
       <Card style={{ width: '18rem', height: '24rem' }} className="mb-3">
           <Card.Img style={{ height: '14rem' }}  variant="top" src={props.mascota.foto} alt={props.mascota.descripcion} />
           <Card.Body>
               <Card.Title>{props.mascota.nombre}</Card.Title>
               <Card.Text>
                   {props.mascota.descripcion}
               </Card.Text>
           </Card.Body>
       </Card>
   );
}

export default Mascota;

Este componente recibe como parámetro (vía props) los datos de una mascota. En la vista se renderiza una tarjeta de Bootstrap con la imagen de la mascota, su nombre y su descripción.

Un esquema (layout) recurrente en las aplicaciones web es la presencia de un banner, un menú de navegación, una parte central donde se renderiza la mayoría del contenido y un footer.

La parte correspondiente al contenido cambia dependiendo de la ruta a donde navegue el usuario o a la opción del menú que haya seleccionado.

Para implementar este esquema usaremos la librería React Router, la cual instalamos con el siguiente comando: npm install react-router-dom --save.

Tomaremos como referencia el ejemplo que desarrollamos en el tutorial del hook de efecto. Este será entonces el componente principal (app.js):

import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./App.css";
import Detail from "./components/detail";
import Mascotas from "./components/mascotas";
import NavBar from "./components/navbar";

function App() {
 return (
   <div className="App">
     <NavBar></NavBar>
     <BrowserRouter>
       <Routes>
         <Route path="/" element={<Mascotas />} />
         <Route path="/mascotas" element={<Mascotas />} />
         <Route path="/mascotas/:mascotaId" element={<Detail />} />
       </Routes>
     </BrowserRouter>
   </div>
 );
}

export default App;

La modificación que hemos incluido acá es el uso de un componente denominado NavBar, el cual corresponde a la barra de navegación. Luego usamos el componente BrowserRouter que será el encargado de manejar la navegación. Dentro del componente Routes incluimos las rutas de la aplicación:

El componente NavBar tiene el siguiente contenido:

import Container from "react-bootstrap/Container";
import Navbar from "react-bootstrap/Navbar";

function NavBar() {
 return (
   <>
     <Navbar bg="dark" variant="dark">
       <Container>
         <Navbar.Brand href="/mascotas">Adóptame</Navbar.Brand>
       </Container>
     </Navbar>
   </>
 );
}

export default NavBar;

Acá usamos un componente de Bootstrap para mostrar una barra de navegación.

Este es el contenido del componente Mascotas:

import Mascota from "./mascota";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";

const { useEffect, useState } = require("react");

function Mascotas() {
 const [mascotas, setMascotas] = useState([]);
 useEffect(() => {
   const URL =
     "https://gist.githubusercontent.com/josejbocanegra/829a853c6c68880477697acd0490cecc/raw/99c31372b4d419a855e53f0e891246f313a71b20/mascotas.json";
   fetch(URL)
     .then((data) => data.json())
     .then((data) => {
       setMascotas(data);
     });
 }, []);

 return (
   <div className="container">
     <h2 className="mt-2">Listado de mascotas</h2>
     <hr></hr>
     <Row>
       {mascotas.map((mascota) => (
         <Col key={mascota.id}>
           <Mascota mascota={mascota} />
         </Col>
       ))}
     </Row>
   </div>
 );
}

export default Mascotas;

Este es el contenido del componente mascota:

import Card from "react-bootstrap/Card";
import { Link } from "react-router-dom";

function Mascota(props) {
 return (
   <Card style={{ width: "18rem", height: "24rem" }} className="mb-3">
     <Card.Img
       style={{ height: "14rem" }}
       variant="top"
       src={props.mascota.foto}
       alt={props.mascota.descripcion}
     />
     <Card.Body>
       <Card.Title>
         <Link to={"/mascotas/" + props.mascota.id}>
           {props.mascota.nombre}
         </Link>
       </Card.Title>
       <Card.Text>{props.mascota.descripcion}</Card.Text>
     </Card.Body>
   </Card>
 );
}

export default Mascota;

Acá podemos observar que en el nombre de la mascota hemos agregado un nuevo componente denominado Link, que navegará a la ruta /mascotas/ más el id de la mascota. Cuando se usa Link la navegación actualizará solo una parte de la página y no la página completa.

Este es el contenido del componente Detail:

import { useParams } from "react-router-dom";
export default function Detail() {
 const params = useParams();
 return (
   <div>
     <h1>I am {params.mascotaId}</h1>
   </div>
 );
}

En este componente usamos el hook useParams que captura los parámetros de la url. El componente mostrará entonces el valor del parámetro mascotaId.

Reto:

Modifique el componente Detail para mostrar los detalles de una mascota así: