React Router te permite renderizar un componente concreto en función del "path" de la url y navegar entre componentes.

Introducción

React Router te permite renderizar un componente concreto en función del "path" de la url.

Esto se consigue creando un conjunto de rutas, en que cada ruta és un "mapping" entre el path de una URL i un componente:

flowchart LR
    Path --route--> Component
Sitio Web https://react-router-7f6ae6.gitlab.io
Proyecto https://gitlab.com/xtec/typescript/react-router

Entorn de trabajo

Crea un proyecto con Bun

bun create vite router --template react-swc-ts
cd router
bun update

Instala la biblioteca react-router:

bun add react-router
bun add -d @types/react-router

Ruta

Imagina que tienes un componente Student (modifica el fichero App.tsx).

export default function App() {

  return <Student/>
}

function Student() {
  return <h1>Student</h1>
}

Inicia Vite:

bun run dev

Si abres el navegador en el path / (http://localhost:5173/) se rendizará el componente Student.

Lo curioso es que si abres el navegar en el path /hello (http://localhost:5173/student) también se rendizará el componente Student

Lo que vas a hacer a continuación es que el componente App en lugar de devolver el componente Student devuelva el componente BrowserRouter.

import {BrowserRouter, Routes} from "react-router"

export default function App() {

  return (
    <BrowserRouter>
      <Routes>
      </Routes>
    </BrowserRouter>)
}

Como no has definido ninguna ruta, el componete BrowserRouter devuelve un contenido vacío para el path /: </>

Añade esta ruta a Routes: / -> `:

import {BrowserRouter, Routes, Route} from "react-router"

export default function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path={"/"} element={<Student/>}/>
      </Routes>
    </BrowserRouter>)
}

Ahora, como el componente BrowserRouter tiene definida una ruta para el path / que indica que tiene que devolver el componente Student, si que se renderiza el componente Student para la url http://localhost:5173/.

Y además, el componente Student se renderiza únicamente para ese path a diferencia de lo que pasaba al principio.

Modifica la ruta para que el componente Student se renderize en el path /student.

Verifica que funciona: http://localhost:5173/student

// ...

export default function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path={"/student"} element={<Student/>}/>
      </Routes>
    </BrowserRouter>)
}

Como puedes intuir, dentro del componente Routes puedes poner más componenetes Route que apuntan al mismo o a otros componentes.

Crea un componente Home, y añade una ruta de / a <Home/>:

// ...

export default function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path={"/"} element={<Home/>}/>
        <Route path={"/student"} element={<Student/>}/>
      </Routes>
    </BrowserRouter>)
}

function Home() {
  return <h1>Home</h1>
}

Segmento dinámico

La mayoría de los componentes utitlizan props para crear su contenido.

Modifica el componente Student para que utilice la propiedad name

function Student({name}: { name: string }) {
  return <h1>Student: {name}</h1>
}

Ahora la ruta /student dará error porque el componente Student requiere como parámetro la propiedad name.

Ningun problema 🐱!

         // ...
        <Route path={"/student"} element={<Student name={"Laura"}/>}/>

Verifica que la ruta vuelve a funcionar: http://localhost:5173/student

El problema es que esta ruta solo sirve para Laura 👩‍🦰

El path de una URL está formada por segmentos que se dividen con /, y si un segmento comienza con : se convierte en un "segmento dinámico".

Por ejemplo, en /student/:name, /:name és un segmento dinámico.

Se llama dinámico porque admite cualquier valor y se proporciona al componente como un parámetro.

Modifica la ruta y elimina la prop name del componente Student 😯

         // ...
        <Route path={"/student/:name"} element={<Student/>}/>

// ...

function Student() {
  return (<h1>Student: Laura 👩‍🦰</h1>)
}

Verifica que el router funciona para cualquier path /student/:name como http://localhost:5173/laura, http://localhost:5173/eva, etc...

... i que siempre renderiza el mismo estudiante "Laura 👩‍🦰"

Entonces, resulta que ahora:

  • Tienes configurado el componente BrowserRouter con un componente Route .
  • Para que devuelva el componene Student cuando el path del navegador coincida con /student/:name.
  • Donde :name puede ser lo que el usuario quiera.

El único problema que tienes que resolver es como puede el componente Student acceder al parámetro name ?

Para esto está la función useParams():

function Student() {
  const {name} = useParams()
  return (<h1>Student: {name}</h1>)
}

I ahora si vas a http://localhost:5173/eva la pagina renderiza a la estudiante "eva 👩‍🦰"

Crea el fichero data.ts con algunos estudiantes:

export type Student = {
    id: number
    emoji: string
    name: string
    surname: string
}

export const students: Student[] = [
    {id: 1, emoji: "👩‍🦰", name: "Mary", surname: "Sinclair"},
    {id: 2, emoji: "🧙‍♂️", name: "John", surname: "Smith"},
    {id: 3, emoji: "🐱", name: "Olivia", surname: "Hart"}
]

Modifica la ruta de /student/:name a /student/:id para utilizar el id de estudiante en lugar del name:

         // ...
        <Route path={"/student/:id"} element={<Student/>}/>

Modifica el componente Student para que busque el estudiante en la lista students de data.tsx:

import {students} from "./data.ts";

// ...

function Student() {

  const { id } = useParams()

  const student = students.find(student => student.id == Number(id))
  if (student == null) {
    return <p>Student not found!</p>
  }

  return <p>{student.emoji} {student.name} {student.surname}</p>
}

Verifica que ahora esta url http://localhost:5173/student/1 responde con "👩‍🦰 Mary Sinclair"

Añade una nueva ruta:

export default function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path={"/"} element={<Home/>}/>
        <Route path={"/student/"} element={<Students/>}/>
        <Route path={"/student/:id"} element={<Student/>}/>
      </Routes>
    </BrowserRouter>)
}

Crea el componente Students que devuelva una tabla con todos los estudiantes:

IDEmojiSurnameName
1👩‍🦰SinclairMary
2🧙‍♂️SmithJohn
3🐱HartOlivia

function Students() {

  return (
    <div className="container m-5 w-50">
      <h3>Students</h3>
      <table className={"table table-bordered mt-5"}>
        <tr><th>ID</th><th>Emoji</th><th>Surname</th><th>Name</th></tr>
        {students.map(student => <tr>
          <td>{student.id}</td>
          <td className={"fs-4"}>{student.emoji}</td>
          <td>{student.surname}</td>
          <td>{student.name}</td>
        </tr>)}
      </table>
    </div>
  )
}

Una aplicación debe tener enlaces para que el usuario pueda navegar de una página a otra página.

Modifica el componente Home y añade un enlace normal a /students:

function Home() {
  return <a href="/student/">Students</a></li>
}

Pero al ejemplo le falta un poco de estilo 🧙‍♂️ 💫

function Home() {

  return (

    <div className={"container m-5"}>

      <ul className="nav nav-underline mb-3 justify-content-center">
        <li className="nav-item">
          <a className="nav-link active" aria-current="page" href={"/"}>Home</a>
        </li>
        <li className="nav-item">
          <a className="nav-link active" href={"/student/"}>Students</a>
        </li>
      </ul>

      <h3>Patufet School</h3>

    </div>
  )

Cuando el usuaio hace clic en el enlace, el servidor responde devolvieno otra vez toda la aplicación, y como el path a cambiiado BrowserRouter devuelve el componente correspodiente.

De funcionar funciona, funciona bien de momento, pero no es nada eficiente 🤔!

React router proporciona el componente Link, que es un "wrapper" de <a>, que lo que hace es impedir que el navegador ejecute el enlace, y en su lugar le dice al BrowserRouter que actualice el contenido a la nueva ruta.

De esa manera, aunque parezca que estamos navegndo de página a página, en realidad estamos navegando de componente a componente 😀!

Modifica el enlace:

function Home() {
  return (
    // ...
          <Link className="nav-link active" aria-current="page" to={"/"}>Home</Link>
}

Ves al home (/), para el servidor, y verifica que los enlaces funcionan!

19:09:29 [vite] (client) hmr update /src/App.tsx (x22)
error: script "dev" exited with code 58
PS C:\Users\david\react-router>

Este tipo de navegación se llama "client-site routing" porque, aunque el navegador muestre una url diferente, estas navegando de un componente a otro componente detro de la aplicación.

Vuelve a arrancar el servidor:

> bun run dev
...

A continuación modifica el componente <Students> de tal manera que cada estudiante de la tabla tenga un enlace a la ruta <Route path={"/student/:id"} element={<Student/>}/>.

De esta manera el usuario puede hacer clic y ir a la "pàgina" del estudiante:

      // ...
      {students.map(student => <tr>
          <td>
            <Link to={"/student/" + student.id} className={"btn btn-primary btn-large"}>{student.id}</Link>
          </td>
          <td className={"fs-4"}>{student.emoji}</td>

Crea un componente Navbar con un barra de navegación de Boostrap - Navbar

import {NavLink} from "react-router";

export default function Navbar() {
    return (
        <nav className="navbar navbar-expand-lg bg-body-tertiary">
            <div className="container-fluid">
                <NavLink className="navbar-brand" to="/">Patufet</NavLink>
                // ...

Añade el componente Navbar a los componentes Home, Student i Students.

function Home() {
  return (
    <>
      <Navbar/>
      <div className={"container m-5"}>
      // ...

Página web

Com has visto, React te permite construir aplicaciones clientes que no necesitan un servidor.

Abre el terminal y ejecuta:

> bun run build

vite v6.0.3 building for production...

✓ 26 modules transformed.
dist/index.html                  0.48 kB │ gzip:  0.32 kB
dist/assets/index-COkpbCJz.js  145.81 kB │ gzip: 47.24 kB
✓ built in 707ms

En el directorio dist están todos los ficheros necesarios para ejectuar la aplicación.

A continuación subirás el contenido a un alojamiento web:

Regístrate en Netlifly.

Crea un "new team" con un plan grautio:

Sube el directorio dist que acabas de crear:

Ves a la página del sitio (y cambia el nombre si quieres):

En el aparece la URL del sitio accesible desde cualquier navegador conectado a Internet!

Actividades

Teachers

Modifica el fichero data.ts y añade unos cuantos profesores a la escuela:

export type Teacher = {
    id: string,
    name: string,
    avatar: string
}

export const teachers: Teacher[] = [
    {id: "pmoretti", name: "Pau Moretti", avatar: "https://i.pravatar.cc/100?img=11"},
    {id: "lsoler", name: "Laia Soler", avatar: "https://i.pravatar.cc/100?img=5"},
    {id: "msimon", name: "Marc Simon", avatar: "https://i.pravatar.cc/100?img=8"},
]

Crea el fichero teachers.tsx con los componentes Teacher y Teachers:

export function Teachers() {
  return (
    <div className="container py-4">
      <h2 className="text-center mb-4">Teachers</h2>
      // ...
    </div>
  )
}

// ...

Añade las rutas /teacher/ y /teacher/:id al componete BrowserRouter.

export default function App() {
  return (
    <BrowserRouter>
      <Menu />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/student/" element={<Students />} />
        <Route path="/student/:id" element={<Student />} />
        <Route path="/teacher/" element={<Teachers />} />
        <Route path="/teacher/:id" element={<Teachers />} />
      </Routes>
    </BrowserRouter>
  )
}

Sites

Añade una ruta "Not Found"

export default function App() {

  return (

    <BrowserRouter>
      <Routes>
        <Route path="*" element={<NotFound />} />
        <Route path="/" element={<Home />} />
        // ..
}

function NotFound() {
  return (
    <>
      <p className="text-danger fs-4">404 Not Found</p>
    </>
  )
}

Añade alguna mejoras, por ejemplo:

  • Una pàgina con un mapa de donde está la escuela.
  • etc.

Despliega la nueva version en Netlify.