Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:desarrollo_web_react_18:comunicacion_interaccion_servidor

¡Esta es una revisión vieja del documento!


Comunicación e interacción con el servidor

Notas del curso Desarrollo de aplicaciones web con React 18

Una interfaz bonita y altamente interactiva no sirve de nada si no puede cargar y guardar datos en un lugar más permanente. En el caso de una aplicación web, ese lugar sería un servidor o, más concretamente, el backend de la aplicación web.

En el caso de una aplicación web totalmente independiente y sin conexión a ningún servidor, podríamos utilizar el localStorage del navegador como almacenamiento persistente. Su uso es muy simple y la metodología para integrar la carga y actualización de los datos en React/Redux sería muy similar a como lo haremos con un servidor. Aunque, claro, solo se mantendrían los datos en ese mismo navegador y sería conveniente facilitar una manera de exportarlos e importarlos con facilidad.

A lo largo de este módulo, aprenderás cómo establecer una comunicación entre la aplicación y un servidor que presente una API REST. Esto lo haremos tanto con React, para aplicaciones simples, como utilizando Redux Toolkit, para aplicaciones más complejas como nuestro gestor de tareas kanban. Además, nos familiarizaremos con otras técnicas que harán la experiencia de usuario de nuestra aplicación más amigable y completa.

Nuevamente, muchos de los conceptos y técnicas que vas a aprender en este módulo pueden resultarte algo complejos. No te preocupes, es normal. Redux es complicado. Procura ir adelante y atrás para repasar conceptos, comparar el código antes y después de los cambios, probar a crearlo por tu cuenta, etc. En definitiva, invertir tiempo en practicar, que es la única forma de asentar conocimientos. Y para cualquier duda que te surja tras haber hecho todo esto, contacta con el tutor en el foro (para que otros puedan sacar partido a las respuestas y participar) o por mensajería interna (si es una duda que no aporta a los demás o si es sobre una práctica).

Comunicación directa con la API - Ejemplo microblog

Puesto que React es únicamente una biblioteca para la creación de interfaces de usuario interactivas, en principio no impone ninguna restricción acerca de la metodología que debemos usar para cargar y enviar datos a fuentes externas. El único conflicto que podemos llegar a tener es el hecho de que las funciones que escribimos al construir los componentes deben ser puras y no pueden tener efectos secundarios. En particular, realizar peticiones a servicios externos es un efecto secundario.

En esta lección aprenderemos a indicar mediante el hook useEffect los efectos secundarios que queremos que ocurran en el contexto de un componente, de forma que React pueda controlar esos efectos y actualizar la interfaz convenientemente.

Con el objetivo de desarrollar un ejemplo autocontenido, vamos a construir un sistema de microblogging para un único usuario, con capacidad para cargar, editar y enviar publicaciones. Para ello, puedes crear un nuevo proyecto React con npm init vite y eliminar todos los archivos del directorio src salvo App.jsx, index.css y main.jsx.

Simulación de la API REST con json-server

Antes de comenzar a implementar esta aplicación, vamos a definir la fuente de datos que va a consumir. La biblioteca json-server nos aporta funcionalidad de sobra para este propósito, ya que es capaz de simular una API REST a partir de un archivo JSON de datos, e incluso permite algunas peticiones relativamente complejas para realizar diferentes búsquedas y consultas.

Para disponer de un servidor con esta biblioteca, puedes utilizar el archivo db.json que encontrarás en los archivos asociados a la lección, y del que se muestra un extracto a continuación:

{
  "cuentas": [
    {
      "nombre": "Miguel",
      "id": 1
    }
  ],
  "posts": [
    {
      "contenido": "En un lugar de la Mancha, [...] partes de su hacienda.",
      "autoria": 1,
      "id": 12,
      "fecha": "1605-01-01T12:00:00Z"
    },
    {
      "contenido": "El resto della [...] tomaba la podadera.",
      "autoria": 1,
      "id": 13,
      "fecha": "1605-01-02T12:00:00Z"
    },
    {
      "contenido": "Frisaba la edad [...] de la caza.",
      "autoria": 1,
      "id": 14,
      "fecha": "1605-01-03T12:00:00Z"
    }
  ]
}

Es importante que entiendas qué entidades de datos vamos a manejar y cómo se relacionan entre ellas, así que antes de continuar, dedica un tiempo a analizar el archivo db.json y a comprender su estructura.

Para poder ejecutar el servidor, es necesario instalar json-server como dependencia e invocarlo desde la terminal con el comando json-server db.json. Es muy posible que el ejecutable no se encuentre en el PATH, así que lo más cómodo será añadir un script al archivo package.json de nuestra aplicación indicando:

"server": "json-server --watch db.json"

De esta forma, podremos ejecutar npm run server y el gestor de paquetes se encargará de buscar el ejecutable de json-server que necesita para arrancar el servidor. Normalmente, este escuchará por defecto en el puerto 3000, pero se puede modificar si fuera necesario. En cualquier caso, ese puerto habrá que tenerlo en cuenta para que la aplicación React pueda enviar peticiones.

Obtención y manipulación de los datos

Como ya se ha mencionado, React no incorpora herramientas que sirvan expresamente para la interacción con fuentes externas. Por tanto, podemos implementar una interfaz para la API libremente. En este caso, puesto que se trata de un caso sencillo, vamos a recurrir directamente a las utilidades integradas en JavaScript: esencialmente, la función fetch y la gestión de promesas con then.

Para simplificar el código posterior, vamos a crear una función que construya el esqueleto que van a tener todas nuestras peticiones al servidor. Recibirá como parámetros un endpoint (es decir, la ruta a consultar tras la URL base de la API), un método HTTP opcional (por defecto GET) y un cuerpo de la petición opcional. Todas las peticiones se envían y reciben en formato JSON, por lo que hacemos uso de la cabecera HTTP correspondiente y las funcionalidades de JavaScript para convertir objetos a JSON y respuestas JSON a objetos.

const API_BASEURL = "http://localhost:3000"
const api = (endpoint, method = "GET", body) => {
  return fetch(API_BASEURL + endpoint, {
    method,
    headers: {
      'Content-Type': 'application/json'
    },
    body: body ? JSON.stringify(body) : null
  }).then(r => r.json())
}

Hay que notar que esta implementación no estaría completa en el caso de una aplicación para producción, ya que estamos haciendo caso omiso de los posibles errores que puedan producirse en las peticiones, y estamos asumiendo que todas se van a completar correctamente. Lo hacemos en pos de simplificar este ejemplo para centrarnos en lo importante para la formación: integrar la carga y envío de datos en React. Lo otro son cuestiones que cualquier desarrollador competente de JavaScript debe saber hacer.

Habrás notado que la función api no solo envía la petición, sino que devuelve el resultado de llamar a la función fetch(). Este es una promesa, lo cual nos permite encadenar acciones que se mantendrán a la espera de que se complete la petición, y se ejecutarán cuando esta termine. Por ejemplo, si obtenemos las publicaciones del blog, será interesante ordenarlas de forma que las más recientes aparezcan primero:

const getBlog = () => api("/posts").then(ps =>
  ps.sort((a, b) => Date.parse(b.fecha) - Date.parse(a.fecha))
)

De forma similar, definimos funciones que ejecuten llamadas concretas a la API: la consulta de una cuenta de autor, así como la edición, creación y borrado de una publicación:

const getAuthor = autorId => api(`/cuentas/${autorId}`)
const editPost = (id, contenido) => api(`/posts/${id}`, "PATCH", { contenido })
const saveNewPost = contenido => api("/posts", "POST", {
  contenido, autoria: 1, fecha: (new Date()).toISOString()
})
const deletePost = id => api(`/posts/${id}`, "DELETE")

Todas estas funciones nos permiten aislar los efectos secundarios que trataremos de que ocurran en la página web.

El hook useEffect

Dentro de la función que renderiza un componente, los efectos secundarios que queramos que tengan lugar independientemente de posibles eventos tendrán que hacerse a través de un hook denominado useEffect. Esto quiere decir que React ofrece dos ubicaciones para enviar y recibir datos externos:

  • Las funciones manejadoras de eventos
  • Las funciones englobadas en useEffect

Esto es así porque dichas funciones se ejecutan en un contexto diferente y no afectan de forma directa a la renderización del componente, que es lo que debe ser una operación pura.

El hook useEffect no devuelve nada y recibe dos parámetros:

  • setup: debe ser una función que implemente los efectos que deben ocurrir cada vez que se ejecute el efecto. Opcionalmente, puede devolver una función de limpieza que se ejecutará antes de volver a ejecutar el efecto.
  • dependencies: es un parámetro opcional que indica la lista de dependencias del efecto, es decir, qué datos son necesarios para computar dicho efecto de forma que cuando estos cambien, se vuelva a ejecutar de manera automática.

Un ejemplo simple podría ser una página de búsqueda en la que podemos filtrar las publicaciones por varios criterios, siendo uno de ellos el rango de fecha. El efecto principal sería cargar las publicaciones en ese rango y añadirlas a la interfaz. Cuando el intervalo de fechas elegido cambie, habría que limpiar la lista de publicaciones que se está mostrando y volver a cargarla otra vez con el nuevo rango:

useEffect(() => {
  getPublicacionesEntre(fechaInicio, fechaFin).then(
    res => agregarPublicaciones(res)
  )
  return () => {
    limpiarPublicaciones()
  }
}, [fechaInicio, fechaFin])

Utilizando el parámetro dependencies, los efectos se pueden configurar para que tengan lugar en tres casos posibles:

  • Una única vez en la primera renderización del componente: si indicamos un array vacío [].
  • Solamente cuando cambien datos concretos: si especificamos esos datos como un array de elementos, por ejemplo: [a, b]. Esto también ejecuta el efecto en la primera renderización.
  • En cada renderización del componente, si no indicamos el parámetro.

Los efectos que no deben ejecutarse en la primera renderización pero sí deben preceder a la siguiente invocación del efecto principal, irán en la función devuelta por la que pasemos como parámetro a useEffect.

El componente principal App

Una vez que conocemos las técnicas que tenemos a nuestra disposición para generar efectos secundarios, vamos a aplicarlos al caso de nuestra aplicación microblog para rellenar la interfaz con las publicaciones que alberga el servidor:

function App() {
  const [blog, setBlog] = useState([])
  const [obsoleto, set, unset] = useFlag()
  useEffect(() => {
    getBlog().then(setBlog)
    return unset
  }, [obsoleto])

Con esas líneas estamos estableciendo blog como un dato con estado en el componente y la carga del blog al inicio de la aplicación y cada vez que la bandera obsoleto cambie de valor. Aquí, useFlag es un hook personalizado:

const useFlag = (valorDefecto = false) => {
  const [valor, setValor] = useState(valorDefecto)
  return [valor, () => setValor(true), () => setValor(false)]
}

Como ves, utiliza por debajo useState() y nos permite activar o desactivar un indicador (también llamado “flag” o “bandera” habitualmente) para avisar de algún cambio en el estado.

En la línea 6 del primer fragmento, antes de ejecutar el efecto principal donde cargamos y mostramos el blog, se devuelve el método unset, de modo que se desactiva la bandera para que los siguientes avisos tengan efecto.

Recuerda que la función de setup que se le pasa a useEffect() puede devolver una función de limpieza, que se ejecutará antes de volver a ejecutar el efecto. Es justo lo que hacemos en este caso devolviendo set.

Además, vamos a permitir crear nuevas publicaciones, con lo que necesitaremos un formulario y una función para gestionarlo. Observa que, una vez que se crea la nueva publicación en el servidor, se activa la bandera para que el efecto que hemos creado antes se ejecute (con el then de la promesa):

  const [postContent, setPostContent] = useState("")
  const addPost = event => {
    event.preventDefault()
    saveNewPost(postContent).then(set)
    setNewPost("")
  }

Por último, nos queda diseñar la interfaz web de la aplicación. Esta será sencilla, compuesta únicamente del formulario de creación de nuevas publicaciones y el recorrido por la lista de las actuales, creando una instanciación del componente Post para cada una:

  return (
    <div className="App">
      <h1>Microblog personal</h1>
      <form onSubmit={handleSubmit} className='new'>
        <textarea value={postContent} placeholder="Nuevo post"
          onChange={(event) => setPostContent(event.target.value)} />
        <button type="submit">Publicar</button>
      </form>
      {blog.map(p => <Post {...p} key={p.id} onChange={set} /> )}
    </div>
  )
}

Observa que hemos indicado una propiedad onChange que utilizaremos dentro del componente para invocarla cuando la publicación sufra cualquier cambio (edición o borrado). En esos casos, se activa la bandera (set()) y por tanto se vuelve a cargar la lista de publicaciones al entrar en acción el efecto, que depende de la variable obsoleto, establecida por set.

El componente Post

El componente Post se encarga de mostrar el contenido de una publicación y permite editarlo y borrarlo. Además, asociado a cada uno tenemos el identificador de la cuenta que lo ha publicado, así que conviene obtener el nombre para poder incluirlo en los metadatos. Esto lo conseguimos con un efecto en el que solicitamos al servidor los datos de la cuenta referenciada por la propiedad autoria (revisa el archivo db.json, que simula la base de datos con los datos de la aplicación de ejemplo):

function Post({ id, autoria, contenido, fecha, onChange }) {
  const [nombre, setNombre] = useState("Anónimo")
  const [postContent, setPostContent] = useState(contenido)
  const fechaObj = new Date(fecha)
  const handleSubmit = event => {
    event.preventDefault()
    editPost(id, postContent).then(onChange)
  }
  useEffect(() => {
    getAuthor(autoria).then(n => setNombre(n?.nombre))
  }, [autoria])
 
  return (
    <div className="post">
      <div className="aut">{nombre}</div>
      <div className="date">{fechaObj.getDate()}/{fechaObj.getMonth()+1}/{fechaObj.getFullYear()}</div>
      <div className="text">{contenido}</div>
      <div className="opt">
        <button onClick={() => deletePost(id).then(onChange)}>Eliminar</button>
        <details>
          <summary>Editar...</summary>
          <form onSubmit={handleSubmit}>
            <textarea value={postContent} placeholder="Nuevo post"
              onChange={(event) => setPostContent(event.target.value)} />
            <button type="submit">Publicar edición</button>
          </form>
        </details>
      </div>
    </div>
  )
}

El resultado del código anterior, unido a algo de estilo CSS, lo podremos ver accediendo a nuestra aplicación arrancada con Vite, como en la captura siguiente:

Como ves, podemos utilizar cualquier herramienta para comunicar una aplicación React con otros servicios. Lo único que debemos tener en cuenta es que cualquier cambio que afecte a la entrada y salida de datos de la aplicación habrá que efectuarlo en una función gestora de eventos o como un efecto con useEffect.

Vamos a verlo en la práctica para acabar de asentar los conceptos. Eso sí, procura repetir por tu cuenta los pasos que vamos a seguir en el vídeo, antes de seguir con el resto del módulo….

Peticiones en aplicaciones con Redux: el asunc thunk

Comunicación mediante RTK Query - Aplicación al gestor Kanban

Estilos con Tailwind CSS

Componentes con ranuras para elementos

Composición de componentes y componentes de orden superior

Prácticas propuestas para el módulo

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