React, parte 6: Ruteador

En esta ocasión veremos cómo podemos hacer para cargar páginas dependiendo de la URL. Para eso vamos a descargarnos una dependencia llamada: React Router Dom.

Vamos a instalarlo, entramos a nuestra terminal, al proyecto:

cd mi-primer-proyecto-react

Y lo instalamos:

npm install react-router-dom

(Si tenemos una terminal corriendo con npm run dev, no la cerremos, abramos otra consola)

Vamos a conservar los componentes que habíamos creado en la publicación anterior:

PostList.jsx:

import { useEffect, useState } from "react"
import { PostItem } from "./PostItem";

export const PostsList = () => {

    //Opciones del combo.
    const [filtro, setFiltro] = useState("");
    //La información que me devolverá el API.
    const [lista, setLista] = useState([]);

    useEffect(() => {
        mostrarTareas();
    }, [filtro])

    const mostrarTareas = async () => {
        let url = 'https://jsonplaceholder.typicode.com/todos';
        if(filtro == "completas"){
            url += '?completed=true';
        }else if(filtro == "incompletas"){
            url += '?completed=false';
        }
        try{
            let request = await fetch(url);
            let response = await request.json();
            console.log(response);
            setLista(response);
        }catch(e){
            alert("Error al intentar recuperar las tareas");
        }
    }

    return (
        <form>
            <h2>Ejemplo de listas</h2>
            <select className="form-select mb-3" value={filtro} onChange={e => setFiltro(e.target.value)}>
                <option value=""> Mostrar todo </option>
                <option value="completas"> Mostrar completas </option>
                <option value="incompletas"> Mostrar incompletas </option>
            </select>
            <ul className="list-group">
                {
                    lista.map((item) => (
                        <PostItem key={item.id} title={item.title} completed={item.completed} />
                    ))
                }
            </ul>
        </form>
    )

}

Y el componente hijo PostsList.jsx:

export const PostItem = ( {title, completed} ) => {

    const getClass = () => {
        return (completed) ? "list-group-item list-group-item-success" : "list-group-item list-group-item-danger";
    }
    
    return (
        <li className={getClass()}>
            {title}
        </li>
    )

}

También vamos a agregar dos páginas nuevas: una página principal y otra de info.

Inicio.jsx:

export const Inicio = () => {

    return (
        <>
            <h2> Inicio </h2>
            <p> Ésta es la página de incio </p>
        </>
    );

}

Info.jsx:

export const Info = () => {

    return (
        <>
            <h2> Info </h2>
            <p> Me llamo Fernando, soy desarrollador web y docente. </p>
            <p> El objetivo de este blog es compartir mi experiencia para toda/os aquella/os que se estén iniciando en el mundo del desarrollo web. </p>
        </>
    );

}

Definiendo nuestras rutas

Vamos a ir a nuestro archivo main.js e importamos lo siguiente:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

También vamos a agregar los componentes que representarán nuestras páginas:

//Páginas
import { PostsList } from './components/PostsList'
import { Inicio } from './components/Inicio'
import { Info } from './components/Inicio'

Y definimos nuestro ruteador:

<Router>      
    <div className='container mt-3'>
        <Routes>
          <Route path='/' element={<Inicio />} />
          <Route path='/info' element={<Info />} />
          <Route path='/posts' element={<PostsList />} />
        </Routes>
    </div>
</Router>

El componente <Router> definirá el alcance de nuestro ruteador.

Mientras que <Routes> contendrá la lista, en donde cada ítem será un componente <Route> en donde vamos a indicar:

path

  • path: El path de nuestra apalicación
  • element: El componente que se va a cargar cada vez que se accede a ese path

Error 404

¿Ahora, qué pasa si ingresamos un path que no se encuentra en la lista?

Para eso podemos crear un componente nuevo: Error404.jsx con el siguiente código:

export const Error404 = () => {
    
    return (
        <>
            <h2> Error 404 </h2>
            <p> La página no existe o ha dejado de existir </p>
        </>
    );

}

Y lo agregamos el siguiente item a la lista de rutas:

<Route path='*' element={<Error404 />} />

(Acordarse de importar el componente: import { Error404 } from ‘./components/Error404’)

Menu de navegación: Simple page vs multiple page application

Ahora necesitamos un menú, algún componente para hacer usable la navegación. Vamos a crear nuestro componente Menu.jsx:

import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';

export const Menu = () => {
    return (
        <Navbar expand="lg" className="bg-body-tertiary">
            <Container>
                <Navbar.Brand href="#home">React-Bootstrap</Navbar.Brand>
                <Navbar.Toggle aria-controls="basic-navbar-nav" />
                <Navbar.Collapse id="basic-navbar-nav">
                    <Nav className="me-auto">
                        <Nav.Link href="/">Inicio</Nav.Link>
                        <Nav.Link href="/info">Info</Nav.Link>
                        <Nav.Link href="/posts">Blog</Nav.Link>
                    </Nav>
                </Navbar.Collapse>
            </Container>
        </Navbar>
    );
}

(Componente de React / Bootstrap)

Y lo incluimos en nuestro archivo main.jsx:

<Menu />

(Tiene que estar dentro de <Router>)

Enlaces simple page

Aunque el menú funciona correctamente, como podemos ver al hacer click en cada enlace, la página se recarga. Esto no está mal, porque así funcionan los sitios web tradiciones desde siempre.

Sin embargo existe la posibidad de crear una simple page, que entre otras tendrá la ventaja de mantener información durante la navegación del usuario, sin tener que recurrir a ayudas persistentes como localStorage.

Para eso vamos a usar un componente llamado <Link> Cambiamos el menu con el siguiente código:

import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { Link } from 'react-router-dom';

export const Menu = () => {
    return (
        <Navbar expand="lg" className="bg-body-tertiary">
            <Container>
                <Navbar.Brand href="#home">React-Bootstrap</Navbar.Brand>
                <Navbar.Toggle aria-controls="basic-navbar-nav" />
                <Navbar.Collapse id="basic-navbar-nav">
                    <Nav className="me-auto">
                        <Link className="nav-link" to="/"> Inicio </Link>
                        <Link className="nav-link" to="/info"> Inicio </Link>
                        <Link className="nav-link" to="/posts"> Blog </Link>
                    </Nav>
                </Navbar.Collapse>
            </Container>
        </Navbar>
    );
}

Como podemos ver el elemento <Link> tiene un atributo to, el cual nos permite indicar hacia dónde tiene que ir.

La página no se recarga, pero la URL si lo hace.

Agregar información en la URL

Podemos recuperar información desde la URL como cualquier aplicación tradicional:

En main.jsx:

<Route path='/posts/:idPost' element={<PostShow />} />

Vamos a crear el componente <PostShow /> el cual nos va a mostrar un post en particular.

PostShow.jsx:

import { useState, useEffect } from "react";
import { useParams } from "react-router-dom"

export const PostShow = () => {

    //Recuperamos el id por la URL.
    const {idPost} = useParams();

    const [post, setPost] = useState(null);

    useEffect(() => {
        mostrarTarea();
    }, []);

    const mostrarTarea = async () => {
        try{
            let request = await fetch('https://jsonplaceholder.typicode.com/posts/' + idPost);
            let response = await request.json();
            setPost(response);
        }catch(e){
            console.log(e);
            alert("Error al intentar recuperar las tareas");
        }
    }

    return (
        <>
            {(post == null)
            ?
                <div> El post se está cargando </div>                
            :
                <>
                    <h2> {post.title} </h2>
                    <div> {post.body} </div>
                </>
            }
        </>
    )

}

Mediante la función useParams() recuperamos el idPost:

const {idPost} = useParams();

Y lo mandamos de forma dinámico al API:

let request = await fetch('https://jsonplaceholder.typicode.com/posts/' + idPost);

Primero cambiamos el componente PostList.jsx:

{
    lista.map((item) => (
        <PostItem key={item.id} title={item.title} completed={item.completed} />
    ))
}

Por:

{
    lista.map((item) => (
        <PostItem key={item.id} id={item.id} title={item.title} completed={item.completed} />
    ))
}

(Agregamos el id al componente hijo)

Por último agregamos el enlace a la URL en PostItem.jsx:

Cambiamos:

<li className={getClass()}>
   {title}
</li>

Por:

<li className={getClass()}>
    <Link to={`/posts/${id}`}> {title} </Link>
</li>