Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:desarrollo_web_react_18:cuestiones_adicionales_avanzadas

¡Esta es una revisión vieja del documento!


Cuestiones adicionales y avanzadas

Notas del curso Desarrollo de aplicaciones web con React 18

A estas alturas ya conoces todos los conceptos de React que te permiten desarrollar prácticamente cualquier aplicación de Front-End de tipo SPA (Singe Page Application, aplicaciones de una sola página) con esta biblioteca. También has aprendido los conceptos relacionados con la reactividad, así como a implementarlos de manera más sencilla y profesional utilizando Redux. Sabes conectarte con una API para consultar datos y realizar acciones, así como hacer caché de esos datos y actualizarlos en tiempo real.

Sin embargo, React sigue siendo una biblioteca enfocada únicamente en crear componentes de UI reutilizables, por lo que carece de otras funcionalidades que son necesarias para desarrollar aplicaciones completas. En este módulo veremos cómo suplir algunas de estas limitaciones propias de React mediante otras bibliotecas y herramientas complementarias.

En concreto aprenderás a:

  • Gestionar el enrutamiento de una aplicación React mediante React Router, de modo que puedas crear una aplicación de varias páginas sin perder la naturaleza de SPA.
  • Gestión de sesiones de usuario, realizando autenticación y autorización de usuarios, de modo que puedas añadir seguridad en el lado cliente para tu aplicación, mostrando a cada uno lo que le corresponda.

Las aplicaciones que desarrollamos con React suelen seguir un paradigma de una única página (o SPA, single page application). Esto puede presentar limitaciones cuando hay distintas partes de la aplicación que se deben mostrar en diferentes momentos. Una opción sería cargar la interfaz al completo de todas las posibles pantallas y mostrar u ocultar parte de ella con variables booleanas. Sin embargo, esto implica que el navegador potencialmente tendrá que descargar mucha más información de la que se va a usar, ralentizando la carga de la página web.

La solución a esta problemática consiste en utilizar un sistema de enrutamiento (routing) que preserve la naturaleza de SPA de nuestra aplicación React pero que al mismo tiempo nos siga proporcionando rutas únicas para cada parte de la aplicación. El más utilizado y completo viene en forma de librería y se denomina React Router. A lo largo de esta lección aprenderás a manejarla mediante un ejemplo sencillo, y a continuación lo integraremos en nuestra aplicación gestora de tareas.

La idea de esta lección es que vayas creando paso a paso un proyecto desde cero, siguiendo las indicaciones que se van dando en cada apartado, hasta llegar a construir la aplicación completa. Deberás hacerlo así si quieres conocer bien todos los detalles del funcionamiento, ya que es un tema bastante complejo.

Mapeo de rutas

Para comenzar, crea un nuevo proyecto de tipo React con Vite y añade las dependencias de React Router y LocalForage mediante el siguiente comando:

npm install react-router-dom localforage

LocalForage es una biblioteca que nos permite almacenar datos en el navegador de forma persistente y asíncrona, de forma que no se pierdan al recargar la página. Al contrario que usando tan solo localstorage, es capaz de almacenar otros tipos de datos que no sean cadenas de texto. Se utiliza en el archivo gallery.js para guardar alguna información sobre las fotos del ejemplo.

Además, puedes eliminar todos los archivos que se incluyen por defecto en el directorio src/ salvo main.jsx e index.css, que los mantendremos para modificarlos. Añade los siguientes imports al archivo main.jsx:

import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider
} from "react-router-dom"

Encontrarás en los archivos asociados a esta lección un index.css que puedes copiar directamente, ya que no incluye nada relevante a lo que nos ocupa, pero mejorará la estética de la aplicación 😉

El funcionamiento de React Router consiste en asociar diferentes elementos de React a rutas en nuestra aplicación. Cada ruta se distingue por un camino diferente desde la raíz (/).

Por ejemplo, como demostración de esta funcionalidad, vamos a construir una sencilla aplicación para mostrar una galería de fotografías donde los usuarios podrán votar por sus fotos favoritas. Las rutas que definiremos, por tanto, tendrán el siguiente esquema:

  • /: raíz de la aplicación, que mostrará el índice de fotos
  • /photos/:id: página individual para cada fotografía
  • /photos/:id/voted: página que se mostrará cuando se haya votado por una foto

Si traducimos la lista de caminos a un grafo, podría tener el siguiente aspecto. Se han añadido los nombres de los componentes que vamos a implementar en cada ruta y se observa mejor que unas rutas parten de otras previas:

Para definir cada una de estas rutas utilizaremos elementos de tipo <Route>. Las rutas recibirán como propiedades el camino que representan y el elemento que tienen que renderizar. La ventaja de enrutar de esta forma, en lugar de generar y enviar páginas completas, es que, en cada ruta, podemos definir únicamente las partes de la aplicación que deben cambiar. Para ello, simplemente anidaremos unas rutas dentro de otras y se renderizarán los elementos en el lugar apropiado que marcaremos más adelante.

Tal y como hemos definido el esquema de rutas previamente, escribimos tantos elementos <Route> como rutas incluidas en el índice (que se marcan con la propiedad index) como en el listado que veremos en breve.

Además, especificaremos el parámetro :imageId en la ruta photos/:imageId de forma que, al ir precedido de dos puntos, capturará todas las rutas que empiecen por photos/ y lo que siga se incluirá como parámetro.

Estas rutas las pasaremos por la función createRoutesFromElements() para que se conviertan en un parámetro apto para crear el objeto enrutador mediante createBrowserRouter():

const router = createBrowserRouter(createRoutesFromElements(
  <Route path="/" element={<Root />}>
    <Route index element={<Index />} />
    <Route path="photos/:imageId" element={<Photo />}>
      <Route index element={<Vote />} />
      <Route path="voted" element={<Voted />} />
    </Route>
  </Route>
))

Los caminos de rutas anidadas se anidarán también: por ejemplo, si la ruta de camino voted está anidada bajo la de camino photos/:imageId, entonces llegaremos a la primera cuando la URL tenga el formato /photos/:imageId/voted.

El browser router que hemos instanciado es un enrutador que utiliza la URL del navegador para controlar el contenido de la página, pero también podríamos utilizar createMemoryRouter() si queremos que dicha URL no se modifique (o estamos desarrollando en un entorno en el que no existe tal URL, por ejemplo, una aplicación de escritorio), o createHashRouter() si fuera necesario prefijar una almohadilla # a la ruta en la URL, por ejemplo, cuando no permitamos en el servidor peticiones a rutas distintas de la raíz de la aplicación.

Para utilizar el router que acabamos de crear, sustituiremos el elemento <App> que se renderiza en la raíz de la aplicación por el <RouterProvider> asociado a nuestro objeto:

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

Estructura de los componentes

Ahora que tenemos las rutas definidas, podemos construir la interfaz de la página a partir de diferentes componentes que se renderizarán o no en función de la URL en la que se encuentre la persona visitante en cada momento. Estos se compondrán de sintaxis JSX y los elementos que acostumbramos a usar hasta ahora, con la adición de un nuevo elemento especial: <Outlet>. Este servirá de ranura o slot donde renderizar los elementos correspondientes a las rutas anidadas, en función de en qué ruta nos encontremos.

Primero, el componente Root describirá la estructura general de la página mediante una cabecera y un pie, y establecerá un espacio para el contenido:

// routes/root.jsx
export default function Root() {
  return (
    <>
      <header>
        <h1><Link to="/">Concurso de fotografía</Link></h1>
      </header>
      <main><Outlet /></main>
      <footer>
        Todas las fotografías &copy; sus respectivos autores. Utilizadas bajo la licencia Unsplash.
      </footer>
    </>
  );
}

Habrás notado que también hemos utilizado un elemento <Link> en lugar de un enlace normal con <a>. Esto se debe a que Link es un componente de React Router que realiza la misma función pero sin necesidad de recargar la página. En lugar del atributo href usaremos la propiedad to para indicar el destino del enlace.

Si nos encontramos en la URL raíz /, en ese espacio se mostrará el componente Index que listará las fotografías (del array images que supondremos que está definido) que se pueden visitar:

// routes/index.jsx
export default function Index() {
  // datos de ejemplo
  const images = ["a", "b", "c"]
 
  return (
    <>
      <p className="content">Haz clic en cada imagen para ver más detalles y votar por ella.</p>
      <div id="gallery">
        {images.map((id) =>
          <Link key={id} to={`photos/${id}`}>
            <img src="{`/images/${id}.jpg`}/"> 
          </Link>
        )}
      </div>
    </>
  );
}

Una vez que se accede a la página individual de una fotografía, mostraremos un contenido diferente dando detalles acerca de la misma. Fíjate en que tenemos un nuevo <Outlet> para mostrar las rutas anidadas a la actual (mira el diagrama del principio), que son, o bien el formulario de votación, o bien el mensaje tras haber votado:

// routes/photo.jsx
export default function Photo() {
  // datos de ejemplo
  const title = "Foto de ejemplo"
  const author = "Alice Robertson"
  const votes = 10
  const url = "https://source.unsplash.com/400x400/?cat"
 
  return (
    <figure>
      <figcaption>
        <p><em>{title}</em> por <strong>{author}</strong></p>
        <p className="pill"><span>Votos</span><span>{votes}</span></p>
      </figcaption>
 
      <img src="{url} /"> 
      <Outlet />
    </figure>
  )
}

El formulario de votación es tan sencillo como un formulario <Form> (un componente idéntico a <form> pero que no requiere refrescar la página) con un único botón que sirve para enviar el voto. No es necesario enviar como parámetro la imagen a la que vamos a votar, puesto que ya estará su identificador como parte de la URL de la página cuando se muestre este formulario:

// routes/vote.jsx
export default function Vote() {
  const boton = useRef()
 
  return <div className="cta">
    <p>¿Es tu favorita? Vótala:</p>
    <Form method="post" onSubmit={() => boton.current.disabled="disabled"}>
      <button ref={boton} type="submit">
        Votar
      </button>
    </Form>
  </div>
}

Puedes ver en el componente anterior un ejemplo de uso del hook useRef, que consiste en capturar el nodo DOM correspondiente al botón de “Votar” (marcado para ello con un atributo ref), para desactivarlo en el momento en que se envíe el voto (en el evento submit). De esta forma evitaremos enviar más de un voto seguido por error.

Por último, la interfaz tras haber votado simplemente muestra un mensaje de confirmación y agradecimiento:

// routes/voted.jsx
export default function Voted() {
  return <div className="cta">
    <p>¡Gracias por votar!<br />Tu voto ha quedado registrado</p>
  </div>
}

Si tenemos la aplicación arrancada con npm run dev y accedemos en el navegador a http://localhost:5173/photos/a, veremos una pantalla como la siguiente:

Captura de errores

Por ahora, si tratas de votar una fotografía utilizando el formulario que hemos implementado, verás que se muestra un mensaje de error bastante poco atractivo:

Lo ideal sería poder capturar el error y mostrar un mensaje más amigable dentro de la interfaz de nuestra aplicación. Para ello, utilizaremos una propiedad denominada errorElement donde las rutas aceptan un elemento que se mostrará en caso de que no se pueda mostrar el contenido solicitado u ocurra algún otro error.

Esto nos permite, además, capturar diferentes errores en diferentes partes de la interfaz, y no romper totalmente la estructura de la página en caso de que solamente una sección haya provocado el error.

Para implementarlo, añade un archivo error-page.jsx con el siguiente código para construir un componente que muestre un mensaje de error:

import { useRouteError } from "react-router-dom";
 
export default function ErrorPage() {
  const error = useRouteError();
  console.error(error);
 
  return (
    <div id="error-page">
      <h1>¡Oh, no!</h1>
      <p>Lo sentimos, ha ocurrido un error inesperado.</p>
      <p>Mensaje de error: <i>{error.statusText || error.message}</i></p>
    </div>
  );
}

Ahora, en las rutas de main.jsx, podemos importar dicho componente para mostrarlo en la ruta raíz si esta no se puede mostrar, o como el contenido de la ruta en caso de que las rutas hijas sean las que causen el error:

<Route path="/" element={<Root />} errorElement={<ErrorPage />}>
  <Route errorElement={<ErrorPage />}>
    { /* resto de rutas */ }
  </Route>
</Route>

Podrás comprobar cómo, al provocar un error, el mensaje que aparece no rompe completamente con la interfaz de la aplicación ya que se sigue renderizando dentro del componente Root:

Carga de datos

Seguridad: Gestión de cuentas de usuario y sesiones

Prácticas propuestas para el módulo

informatica/programacion/cursos/desarrollo_web_react_18/cuestiones_adicionales_avanzadas.1711112676.txt.gz · Última modificación: por tempwin