¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Conceptos previos fundamentas
Notas pertenecientes al curso Desarrollo de aplicaciones web con React 18
Durante el periodo de Internet conocido como la Web 1.0, los sitios web habituales se componían de páginas estáticas, prácticamente carentes de interactividad salvo por hipervínculos y el ocasional formulario HTML. En esta era, al navegar por la red principalmente encontrábamos páginas estáticas proporcionadas por los servidores tal cual o con pocas modificaciones, como un contador de visitas o la fecha de última actualización. Por lo general, el contenido de estas páginas web lo incorporaban los propios creadores de los sitios.
Con el nacimiento de los primeros sitios web con funcionalidades sociales como Livejournal o Blogger, era imperativo facilitar la entrada de usuarios con menos experiencia o tiempo para desarrollar sus propias webs, por lo que las tecnologías web que permitían añadir interactividad comenzaron a adquirir mayor importancia. Esto, unido al progresivo soporte del estándar ECMAScript en los navegadores populares de la época, propició el uso de una combinación de HTML y guiones ejecutados en el cliente conocida como HTML dinámico (DHTML).
El Document Object Model
Las páginas con DHTML eran capaces de mostrar menús desplegables interactivos, validar formularios antes de enviarlos al servidor, así como animar y modificar el contenido de la web. Esto era gracias a una representación de los elementos HTML en una estructura de datos con forma de árbol conocida como DOM (Document Object Model).
Cada navegador que implementa un DOM expone una interfaz de programación donde, a partir de la palabra reservada document, se puede explorar la estructura de la página web en el estado en que la está mostrando, donde cada elemento con una etiqueta HTML se corresponderá con un nodo. Estos nodos tienen ciertas propiedades que se pueden consultar y modificar, en particular, todos sus atributos HTML, sus clases y sus propiedades de estilo.
IMAAGEN
Una característica del DOM que añade un nivel adicional de interactividad es la posibilidad de asociar respuestas a eventos provocados por la persona visitante, ya sean clics sobre botones, cambios en campos de texto o, más recientemente, toques sobre una pantalla táctil. Cada respuesta se implementa en forma de función callback, es decir, una función que se ejecuta justo después de que ocurra el evento correspondiente y que recibe la información sobre el evento y el elemento sobre el que se ha realizado.
El DOM que conocemos hoy día se implantó a finales de la década de los 90 y principios de los 2000 en los principales navegadores, aunque algunas mecánicas de manipulación de elementos no eran compatibles entre distintos navegadores, lo que dificultaba su uso. En la actualidad, la gran mayoría de características del estándar ECMAScript 6 están implementadas de la misma forma en todos los navegadores modernos y no es necesario preocuparse por detectar el tipo de navegador para ejecutar un código u otro. Ello facilita que las bibliotecas de gestión y manipulación del DOM sean cada vez más avanzadas y haya menos obstáculos a la hora de programar aplicaciones web más sofisticadas.
Reutilización de código en HTML
En cada carga o refresco de una página web, el navegador solicita y renderiza un documento HTML al completo. Si estamos desarrollando un sitio web con varias páginas, es habitual que varias partes de la estructura se repitan, como una cabecera o un pie de página. Desde el punto de vista del desarrollo, no tiene sentido duplicar el código que las conforman, ya que dificultaría el mantenimiento y la actualización de esas secciones en el futuro. Por otro lado, sería conveniente que el navegador actualizase únicamente los elementos que sean diferentes al cambiar el contenido de la página. De esa forma se ahorraría tiempo tanto de transferencia de los datos del servidor al cliente como de renderizado en el cliente.
Para abordar el objetivo de modificar solamente los elementos necesarios al incorporar nuevo contenido en lugar de cargar una página completa, disponemos de los mecanismos de JavaScript asíncrono conocidos como AJAX (asynchronous JavaScript and XML). Estos consisten en combinar la realización de peticiones al servidor desde guiones JavaScript ejecutados en el cliente con la generación y modificación de los nodos del DOM para mostrar la información obtenida.
Idealmente, podríamos aprovechar ambas técnicas para evitar repetir código a la vez que modificamos y actualizamos el contenido de la página sin necesidad de cargarla de nuevo. Conseguir esto en puro HTML y JavaScript sin ayuda de bibliotecas adicionales es un trabajo que rápidamente se vuelve tedioso.
La biblioteca React
React es una biblioteca de JavaScript para construcción de interfaces de usuario, que proporciona la funcionalidad necesaria para diseñar una aplicación web a partir de pequeñas secciones autocontenidas denominadas componentes. Además, esos componentes pueden ir asociados a eventos y mostrar contenido que se actualiza dinámicamente. Las modificaciones en el contenido se realizan de forma que solo se vuelve a renderizar en el navegador los elementos estrictamente necesarios para mostrar los cambios requeridos.
El desarrollo de aplicaciones web con React permite plantear el diseño y la construcción de una manera diferente a la que es necesaria al escribir páginas estáticas, ya que los componentes se pueden ir construyendo unos en función de otros más pequeños, poco a poco conformando la aplicación completa. Esto facilita la colaboración durante el desarrollo, la separación de responsabilidades y la estructuración de los proyectos.
Las principales características y ventajas de React son:
- React permite la creación de componentes reutilizables que encapsulan la funcionalidad y la vista de una parte de la interfaz de usuario. Esto facilita la creación y el mantenimiento de aplicaciones complejas.
- React utiliza un modelo de programación declarativo, lo que significa que los desarrolladores describen cómo debería verse la interfaz de usuario en lugar de cómo se debe construir. Esto hace que el código sea más fácil de entender y mantener.
- React utiliza un flujo de datos unidireccional, lo que significa que los datos fluyen desde el componente padre hasta los componentes hijos. Esto hace que sea más fácil razonar sobre el estado de la aplicación y prevenir errores.
- React utiliza un DOM virtual, que es una representación en memoria del DOM real. Esto permite que React realice actualizaciones de manera más eficiente y mejore el rendimiento de la aplicación.
- React es altamente escalable y se puede utilizar tanto en el lado del cliente como del servidor.
- React es compatible con una amplia variedad de herramientas y librerías complementarias, lo que facilita la integración con otras tecnologías.
En muchas ocasiones se habla de React comparándolo, explícita o implícitamente, con otros frameworks de JavaScript como Angular o Vue.js. Pero en realidad React no es un framework, sino una biblioteca de interfaz de usuario, lo que significa que no incluye muchas de las características que se encuentran en un framework completo y hay que complementarlo si queremos diseñar una aplicación Front-End completa.
Si lo comparamos con los dos frameworks mencionados, podemos destacar las siguientes diferencias:
Angular es un framework completo que incluye muchas características, como enrutamiento, formularios, inyección de dependencias o validación. React, en cambio, se centra en la creación de interfaces de usuario y no incluye estas características de manera predeterminada. Sin embargo, se pueden agregar fácilmente mediante el uso de bibliotecas complementarias. Angular además ofrece una forma de trabajar más estructurada y con más directrices (lo cual a veces gusta a las empresas, porque facilita incorporar personas a los proyectos), mientras que React es más flexible y permite más libertad. Vue.js es una biblioteca que se asemeja a React en muchos aspectos, incluyendo la creación de componentes reutilizables y el uso de un modelo de programación declarativo. Sin embargo, Vue.js utiliza un enfoque más basado en plantillas para la creación de interfaces de usuario, mientras que React utiliza JavaScript puro. Además, Vue.js es más fácil de aprender y de usar que React, pero React es más escalable y tiene una comunidad mucho más grande.
A lo largo de este curso, aprenderás a escribir aplicaciones que hacen uso de React para interactuar con las personas visitantes y a desarrollar un proyecto desde cero utilizando técnicas modernas asociadas a React.
Repaso de JavaScript moderno
Antes de nada, y dado que las necesitarás durante la formación, vamos a dar un repaso a algunas características “modernas” de JavaScript introducidas en el estándar ECMAScript 6 y posteriores y que serán de mucha utilidad para programar aplicaciones con React.
Funciones flecha
Durante mucho tiempo, las funciones anónimas han existido en JavaScript bajo la sintaxis function(){ … }. Esta sintaxis se utilizaba constantemente para definir callbacks puesto que evitaba la necesidad de dar un nombre a la función y ensuciar así el espacio de nombres global.
Las funciones flecha son una evolución de la función anónima que no requiere del uso de la palabra reservada function para su definición, sino simplemente del operador flecha ⇒. A continuación tienes un ejemplo del caso más sencillo, en el que solo se recibe un parámetro y se devuelve como resultado la única expresión que se ejecuta:
// Función clásica function sumarDos(a) { return a + 2; } // Función anónima const sumarDos = function(a) { return a + 2; } // Función flecha const sumarDos = a => a + 2;
Cuando una función flecha recibe más de un parámetro (o un parámetro desestructurado, que veremos más adelante lo que es) es necesario indicarlo entre paréntesis. Análogamente, si la función ejecuta más de una sentencia o no queremos que devuelva ningún valor, el cuerpo de la función irá entre llaves:
const insertaOrdenado = (lista, numero) => { let i = 0; while (lista[i] < numero) i++; lista.splice(i, 0, numero) }
A lo largo del curso, las funciones que definiremos usarán mayoritariamente esta sintaxis, ya que es más simple y se puede reducir a la mínima expresión si la función es una operación sencilla.
Desestructuración de datos
La desestructuración es una sintaxis que ayuda a extraer datos de una estructura como un vector o un objeto y nos evita tener que acceder mediante índices o claves con el operador corchete. Observa el siguiente ejemplo:
const sumaResta = (a, b) => [a + b, a - b] let [suma, resta] = sumaResta(10, 3) resta // => 7
Al declarar suma y resta en una lista y “asignar” el resultado de llamar a la función a esa lista, el intérprete entiende que queremos asociar esos nombres a los elementos del vector resultante. Algo similar podemos conseguir con objetos mediante el uso de las claves:
let menu = { entrante: "salmorejo", primero: "macarrones", segundo: "salmón" } const primerPlato = ({ primero }) => { console.log(primero); } primerPlato(menu) // => "macarrones"
Fíjate en que, en la función primerPlato, únicamente extraemos el valor asociado a la clave primero y hacemos caso omiso del resto del objeto. Es posible incluso dar otro nombre distinto a la variable y asignar un valor por defecto por si no existiese la clave en el objeto:
const maybeNumero = (umbral) => { const rand = Math.random(); if (rand > umbral) { return { numero: rand }; } return {}; } const { numero: superior = 0 } = maybeNumero(0.5);
En el ejemplo anterior, la función maybeNumero devuelve un número aleatorio entre 0 y 1 si es superior al umbral establecido y, en caso contrario, devuelve un objeto vacío. Al recibir el resultado, declaramos una constante superior que tendrá el valor 0 por defecto si el objeto devuelto no incluye la clave numero.
Operador spread
La desestructuración de datos es interesante cuando queremos extraer algunos datos determinados de un objeto o vector, pero puede que también queramos realizar una operación a la inversa, acumular varios datos en un solo identificador que pasaría a ser un vector u objeto. Esto se puede conseguir con el operador spread, muy similar al conocido en otros lenguajes de programación como splat o asterisco. En el caso de JavaScript, el operador consiste en tres puntos suspensivos … y se puede utilizar de varias formas:
const [primero, ...resto] = [1, 1, 2, 3, 5, 8]; resto // => [1, 2, 3, 5, 8] const sumarValores = (...valores) => { return valores.reduce((a, b) => a + b, 0); } sumarValores(10, 25, 7) // => 42
Como ves, por lo general, el operador acumula en un vector la lista de elementos que le asignemos y permite así definir funciones con un número variable de parámetros, entre otras cosas. También funciona con objetos:
const { valor, ...demas } = { valor: 30, otro: 40 } demas // => { otro: 40 }
Cadenas de caracteres plantilla
En JavaScript tradicional, para construir cadenas de caracteres con valores calculados, típicamente se usa el operador + para concatenar varias cadenas. A partir de ES6 disponemos de cadenas plantilla que nos facilitan ese trabajo y son más rápidas de evaluar por el intérprete:
const a = 'Alice'; const greeting = `Hola ${a}!`; console.log(greeting); // "Hola Alice!"
Como se puede observar en el ejemplo, para denotar una cadena plantilla utilizamos los acentos graves en lugar de las comillas y para evaluar un pequeño trozo de código dentro basta con encapsularlo en llaves y precederlo de un dólar: ${nombreVariable}.
Operador ?? y parámetros por defecto
En ocasiones podemos encontrarnos con variables que pueden estar sin definir, por lo que no tienen un valor útil. Por ejemplo, en la siguiente función suma tenemos dos parámetros que intentaremos sumar, lo que causa que, al llamarla sin argumentos, nos devuelva NaN (not a number):
const suma = (a, b) => a + b; suma() // => NaN
Esto ocurre porque tanto la variable a como b están sin definir (evalúan a undefined). Con el operador ?? podemos tomar una variable y, si no está definida o es nula, optar por un valor alternativo:
const suma2 = (a, b) => { a = a ?? 0; b = b ?? 0; return a + b; } suma2() // => 0
Puede que previamente hayas hecho uso del operador || como aquí estamos utilizando ?? pero es importante que conozcas la diferencia: en el caso de ||, un valor que evalúe a falso (como el número cero o el booleano false) también provocará el uso del valor alternativo, incluso si es un valor que queremos permitir.
En el caso específico de los parámetros de las funciones, también tenemos la posibilidad de asociarles un valor por defecto, simplemente con el operador de asignación =:
const suma3 = (a = 0, b = 0) => a + b; suma3() // => 0
Exportación e importación de módulos
A lo largo del curso desarrollaremos nuestra aplicación en forma de pequeños ficheros JavaScript que tendrán ciertas dependencias e incluso podrán depender unos de otros. Cuando un script se incluye en un archivo HTML con el atributo type=“module”, puede hacer uso de la sintaxis import para incluir código de otros módulos:
<script type="module"> import { sumar } from './math-utils.js'; document.querySelector("#root").textContent = sumar(10, 32) </script> <div id="root"></div>
En el módulo que vayamos a importar tiene que haber una declaración export con el objeto o los objetos que se deseen exponer:
// math-utils.js const sumar = (...vals) => vals.reduce((a, b) => a + b, 0); export { sumar };
Hay diferentes variantes de la sintaxis import y export que conviene que conozcas, por ejemplo, la exportación por defecto y la posibilidad de asignar un alias al importar. Para informarte con más detalle, puedes consultar la documentación de import en MDN y la de export.
Promesas con async y await
Las nuevas palabras reservadas async y await permiten programar de forma asíncrona (con promesas) sin tener que crear y gestionar objetos de tipo Promise. En funciones tipo async se puede utilizar la palabra await para detener la ejecución hasta obtener el resultado de una función que devuelve una promesa:
const crearCuenta = async (user, password, email) => { const result = await fetch("https://example.org/cuentas/", { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ user, password, email }) }).then(r => true).catch(r => false) return result }
Conceptos relacionados importantes
Componentes
En React, la interfaz de una página estará dividida en pequeñas porciones autocontenidas. Cada una de ellas puede contener unos datos, una descripción de la interfaz que producen e incluso definir interacciones con el usuario. Estos son los componentes de la página, y son como bloques de construcción con los que se va montando el sitio web.
Los componentes más sencillos se dedicarán simplemente a encapsular un poco de código HTML de forma que no tengamos que repetirlo a lo largo de la página. Basándonos en estos podremos construir componentes más complejos, abarcando a su vez todos en un componente maestro que sería la aplicación global.
Al tener la interfaz separada en diferentes componentes, React puede alterar la página web cuando ocurra un cambio en los datos que se deben mostrar, sin necesidad de refrescar o volver a generar toda la página. El objetivo es realizar las mínimas modificaciones necesarias para que el cliente tenga la información actualizada con el menor retraso posible.
El proceso por el cual React calcula el código HTML a mostrar en la página web y lo actualiza en el DOM se suele denominar renderizado. Diremos, por ejemplo, que un componente que haya cambiado debe volver a renderizarse.
Propiedades y estado
Los componentes pueden tener acceso a dos categorías diferentes de datos:
- Propiedades: por un lado, podrá existir información que provenga de un componente superior (el que los contiene). Estas son las propiedades, que son equiparables a parámetros recibidos por una función. Las propiedades no se pueden modificar dentro del componente, ya que React no puede detectar esos cambios y, en consecuencia, no tendrían efecto en la interfaz web.
- Datos con estado: por otro lado, si un componente es más complejo y tiene elementos que pueden cambiar internamente, hay que llevar un registro de esos cambios para que React sepa cuándo ocurren y por tanto es necesario un nuevo renderizado. Un ejemplo puede ser un interruptor de dos estados, activo e inactivo. Si el usuario interacciona con el interruptor, este cambiará de estado y React deberá reflejar ese cambio. Si el estado del interruptor se almacena dentro del componente, diremos que tiene estado.
La gestión del estado en los diferentes componentes de una aplicación React es una tarea que puede enfocarse con varias metodologías, que se estudiarán y aplicarán más adelante.
Efectos secundarios
En el campo del desarrollo web, tenemos la costumbre de que las funciones que programamos no solo pueden devolver resultados, sino que también pueden actuar sobre los elementos de la página. En términos generales, la mayoría de lenguajes de programación permiten que una función modifique elementos que están fuera de su ámbito. De hecho, en muchos lenguajes esto es parte intrínseca del diseño. Por ejemplo, cualquier lenguaje orientado a objetos permite que una función vinculada a un objeto (un método) modifique el estado de dicho objeto.
Estos comportamientos de alteración del estado externo a la propia función se denominan efectos secundarios (en inglés, side effects). Por lo general, al desarrollar aplicaciones contamos con que las funciones puedan tener este tipo de efectos. Sin embargo, nos pueden complicar algunos aspectos, ya que hemos de tener en cuenta todos los elementos que pueden modificar. Observa este ejemplo sencillo:
var precioBase = 250; function calcularPrecio(descuento) { // efecto secundario: accede a variable global // efecto secundario: modifica variable global precioBase = precioBase * (1 - descuento / 100); // efecto secundario: modifica la página document.querySelector("#precio").textContent = precioBase; return precioBase; }
Si en la lógica de nuestra aplicación llamamos a calcularPrecio, el precio indicado en la variable precioBase se actualizará, al igual que el texto con identificador “precio” en el documento. Esto puede resultar útil si únicamente realizamos una llamada a la función, pero en el momento en que se complique el método por el cual aplicamos o no un descuento, nos podría interesar consultar varias veces esta función. Por ejemplo, ¿qué ocurre si intentamos que el descuento no supere los 20€? Idealmente nos gustaría poder escribir un condicional sencillo como el siguiente:
if (calcularPrecio(21) < precioBase - 20) { // nunca se ejecuta porque la condición no se va a cumplir }
Sorprendentemente, reescribiendo la misma condición a la inversa, el programa funcionará algo mejor:
if (precioBase - 20 > calcularPrecio(21)) { // puede que se ejecute, pero precioBase está modificado! }
Como ves, hay situaciones donde lo que nos interesa al ejecutar una función es que nos dé un resultado seguro, y que no altere las condiciones en que ejecutamos el resto de nuestro código. Los efectos secundarios son el obstáculo principal para este propósito.
Funciones puras
La solución a los conflictos que presentan los efectos secundarios se conoce como función pura. Se denominan así las funciones que únicamente hacen uso de sus parámetros y no utilizan ni alteran información externa. La función calcularPrecio() que hemos visto antes, escrita como función pura, tendría el siguiente aspecto:
function calcularPrecio(precioBase, descuento) { return precioBase * (1 - descuento / 100); }
En este caso, tanto el precio base como el porcentaje de descuento se proporcionan como parámetros, de forma que la función solo depende de esos valores y es totalmente predecible. Además, el estado de nuestra aplicación antes y después de llamar a la función es idéntico.
Evidentemente, hay muchas tareas que no podremos conseguir haciendo uso únicamente de funciones puras. Por ejemplo, cualquiera que implique leer datos externos, mostrar información por pantalla o en la página web, generar números aleatorios, etc. Por tanto, habrá casos donde nos interese escribir funciones puras y otros donde sepamos que las funciones tendrán efectos secundarios.
Lo importante es saber diferenciar cuándo una función tiene o no ese tipo de efectos secundarios.
Objetos de orden superior
En JavaScript, las funciones son objetos de primera clase, lo que quiere decir que podemos trabajar con ellas como con cualquier otro objeto. En particular, podemos almacenar funciones en variables y constantes, pasarlas como parámetros de otras funciones y devolverlas como valor de retorno.
// Pasar funciones como parámetro const repite = (f) => { f(); f() } repite(alert) // genera dos alertas // Devolver funciones const sumaX = (n) => (m) => n + m const sumaDos = sumaX(2) sumaDos(3) // => 5
Fíjate en que en repite(alert) proporcionamos la referencia de la función alert pero no ponemos paréntesis porque no es una llamada a dicha función propiamente. La llamada la ejecuta la función repite donde escribimos f(). Si hubiéramos ejecutado repite(alert()), el valor que se hubiera pasado como parámetro habría sido el de retorno de la función alert, y no la función en sí.
Esto abre las puertas a una clase de funciones que se encarguen de trabajar con otras funciones, bien modificando su comportamiento o aplicándolas de forma reiterada según un criterio. Estas se denominan funciones de orden superior. Un ejemplo sería una función que ejecuta otra y devuelve un valor numérico en función de si acabó con éxito o con error:
const maybe = (f, ...args) => { try { const value = f(...args) return { value, error: null } } catch (error) { return { value: null, error: error.message } } } maybe(JSON.parse, '{"nombre": "Ana", "edad": 32}') // { value: { nombre: 'Ana', edad: 32 }, error: null } maybe(JSON.parse, '{"nombre": "Ana", "edad": 32') // { // value: null, // error: "Expected ',' or '}' after property value in JSON at position 28" // }
Otros ejemplos notables de funciones de orden superior son map y reduce, que veremos en el siguiente apartado de esta lección.
En React también es relativamente común la creación de componentes de orden superior, que funcionan de forma análoga a las funciones de orden superior: son componentes cuya tarea es modificar el comportamiento de otros componentes o añadirles alguna funcionalidad adicional. Más adelante veremos ejemplos y exploraremos la utilidad de los componentes de orden superior.
Métodos ''map'' y ''reduce''
Un aspecto crucial del desarrollo con React es la reutilización de los componentes, de forma que se pueden crear sucesivos componentes del mismo tipo pero con diferentes datos. Una forma sencilla de crear muchos componentes del mismo tipo será aplicar el método map sobre un array de datos.
Por ejemplo, supongamos que vamos a crear la sección de reseñas de un producto, las tenemos en un array comentarios y disponemos de una función InterfazComentario que construye el HTML de cada comentario. Utilizando el método map tendríamos una sentencia tan simple como:
comentarios.map(InterfazComentario)
Por otro lado, en muchas ocasiones será necesario obtener valores agregados de los datos de los que disponemos. Para esto es para lo que está diseñado el método reduce, que es capaz de ir acumulando un valor a la vez que itera por una colección. Imagina que queremos extraer la media de puntuación que han dado las personas que han dejado reseñas en un producto:
const promedio = comentarios.reduce( (acum, actual) => acum + actual.puntos, 0 ) / comentarios.length
Como ves en el ejemplo, la función que proporcionamos a reduce toma el acumulador acum (que tiene un valor inicial de 0, proporcionado justo después al método) y una reseña actual, devolviendo como resultado la suma de los puntos con los puntos acumulados. Al dividirlo entre el número de reseñas obtenemos la puntuación media.
A lo largo del curso haremos uso repetido del método map y del concepto de reducción, por lo que es importante que entiendas para qué sirven y conozcas su funcionamiento.
Ciclo de vida de un componente
Uno de los aspectos más cruciales y que es importante que conozcas para ser capaz de conceptualizar adecuadamente una aplicación web en forma de componentes es su ciclo de vida. En cada framework los componentes actúan y se pueden controlar u operar de una determinada forma, y en React siguen un proceso específico para su creación, actualización y eliminación de la aplicación.
Los componentes en React se implementan en forma de funciones donde podemos organizar qué comportamiento debe ocurrir antes, durante y después de su renderizado en la pantalla.
IMAAAAAAAAGEN con hexágonos
Primer renderizado
Los componentes en React se pueden utilizar como parte de la interfaz de la aplicación, como si fueran un elemento HTML más. Cuando en el código generado para mostrar una página vaya incluido un componente, React ejecutará la función asociada para obtener su estructura, junto a todos los efectos que hayamos establecido para que ocurran la primera vez que se “monta” un componente.
Considera el caso de una aplicación de gestión de tareas colaborativa. En el momento en que se muestre la lista de tareas, un efecto que tendrá lugar será sincronizarla con el estado de la lista en el servidor, para recuperar los nuevos datos en caso de que ocurran modificaciones.
Actualizaciones
Para que un componente sea reutilizable, por lo general necesitamos que haya aspectos que puedan variar de una instanciación a otra. Por ejemplo, el texto de un botón o la imagen y el título de una tarjeta. Además, habrá casos donde el contenido de los elementos deba cambiar, por ejemplo, si se añade o se edita un comentario en una publicación.
En React, tendremos varias formas de manejar los cambios en los datos que forman parte de un elemento. Cuando algún cambio suceda, el componente se volverá a renderizar automáticamente y podremos especificar ciertas operaciones que ocurran junto con cada actualización. En el caso hipotético de la lista de tareas colaborativa, si el usuario actual añade una tarea nueva, un efecto que provocaremos al actualizar el componente será enviar la lista actualizada al servidor.
Desmontaje
Dado que los componentes pueden incluirse unos dentro de otros, cabe la posibilidad de que un elemento se elimine de la interfaz, por ejemplo, al usar navegación por pestañas y pasar de una a otra. En este caso, el elemento pasará por la etapa de “desmontaje” y podremos ejecutar algunos efectos también en este punto.
Si estuviéramos implementando la lista de tareas colaborativa, al salir de la pantalla donde se muestre la lista se desmontaría el componente asociado y provocaríamos el efecto de desconectar la sincronización con el servidor.
Aplicaciones web no reactivas
El estándar de HTML, tal y como está diseñado, permite una interactividad relativamente limitada dentro de una página web, especialmente si evitamos refrescar la página. Es decir, a diferencia de las aplicaciones de escritorio donde podemos visualizar los resultados de interactuar con campos y botones al instante, en un sitio web es más probable que debamos enviar un formulario y esperar a un refresco de la página para verlos.
Esto se debe a que, si toda la lógica de la aplicación está en el servidor, la única manera que hay de mostrar nueva información es realizar una nueva petición y que el servidor devuelva una nueva página. En estos casos, la experiencia de usuario con el sitio web puede no ser satisfactoria, especialmente si estamos tratando de proporcionar servicios de aplicación web.
Veamos un ejemplo muy simple. En pocas líneas de cualquier lenguaje de programación moderno podemos programar una aplicación que almacene una lista donde los ítems se pueden ir añadiendo desde una página web.
Por ejemplo, en Node.js disponemos del framework Express que sirve para desarrollar aplicaciones pequeñas, y podemos prototipar este ejemplo muy rápidamente. La aplicación consiste en un solo fichero app.js, que guardaremos en una carpeta limpia, con el siguiente código:
var express = require("express") var app = express() var listaTareas = [] app.get('/', (req, res) => { res.send(`<!DOCTYPE html> <title>Gestor de tareas (solo servidor)</title> <header> <h1>Gestor de tareas</h1> <p> Este gestor de tareas consiste en una página HTML estática que se comunica con un servidor. Esta aplicación no dispone de JavaScript en el lado del cliente para actualizar ni modificar la información </p> </header> <main> <h2>Tareas disponibles</h2> <ul>${listaTareas.map(({ titulo }) => `<li>${titulo}</li>`).join("")}</ul> <form action="/add" method="GET"> <input type="text" name="titulo" placeholder="Nueva tarea" autofocus> <input type="submit" value="Añadir"> </form> </main>`) }) app.get('/add', (req, res) => { // simula un tiempo de respuesta lento setTimeout(() => res.redirect('/'), 1000) listaTareas.push(req.query) }) app.listen(4000)
Para instalar Express ejecutaremos el comando npm i -D express. Ahora basta con lanzar el servidor con el comando node app.js y veremos una pantalla similar a la siguiente imagen al acceder con el navegador a localhost:4000.
La única interactividad en esta página rudimentaria es el formulario que permite agregar una nueva tarea. Si se envía, el navegador fuerza la recarga de la página, con lo que se vuelve a pedir la información al servidor, se sustituye la página actual por la nueva versión y finalmente se muestra la lista actualizada.
Esto presenta una serie de desventajas. Por un lado, existe un retardo apreciable entre que se interacciona con la interfaz web y que se muestra algún resultado de dicha acción, lo cual puede desorientar a la persona que esté esperando ver algún efecto en su pantalla. Por otro lado, es altamente ineficiente generar, enviar por Internet y renderizar la página entera en el navegador únicamente para añadir o eliminar una línea en la lista de elementos.
La única forma de superar estos obstáculos es añadir funcionalidad a la página web de forma que la información que muestra cambie en función de las acciones que realicemos sobre ella. Para ello, será necesario escribir lógica en el lado del cliente.
El gestor de tareas basado únicamente en un servidor y sin reactividad
La única interactividad en esta página rudimentaria es el formulario que permite agregar una nueva tarea. Si se envía, el navegador fuerza la recarga de la página, con lo que se vuelve a pedir la información al servidor, se sustituye la página actual por la nueva versión y finalmente se muestra la lista actualizada.
Esto presenta una serie de desventajas. Por un lado, existe un retardo apreciable entre que se interacciona con la interfaz web y que se muestra algún resultado de dicha acción, lo cual puede desorientar a la persona que esté esperando ver algún efecto en su pantalla. Por otro lado, es altamente ineficiente generar, enviar por Internet y renderizar la página entera en el navegador únicamente para añadir o eliminar una línea en la lista de elementos.
La única forma de superar estos obstáculos es añadir funcionalidad a la página web de forma que la información que muestra cambie en función de las acciones que realicemos sobre ella. Para ello, será necesario escribir lógica en el lado del cliente.
Aplicación reactiva sin React
Vamos a dar un paso más allá de la página web básica en HTML añadiendo cierta reactividad mediante código JavaScript en el lado del cliente. Esto quiere decir que, en esta ocasión, la lógica de la actualización de la lista de tareas se estará ejecutando en el navegador y será este quien se encargue de modificar los elementos de la página que sean necesarios para mostrar la nueva información, sin necesidad de recargar la página.
El contenido principal de la página consistirá ahora en una lista vacía y el formulario para añadir tareas. Observa que hemos eliminado la action y el method del formulario puesto que ya no enviaremos de esa forma la información al servidor. Hemos añadido un id a la lista y al formulario para poder identificarlos unívocamente en el guion.
<ul id="lista"></ul> <form id="form"> <input type="text" name="titulo" placeholder="Nueva tarea" autofocus> <input type="submit" value="Añadir"> </form>
A su vez, el código que correspondería a la parte interesante de la aplicación, es decir, el guion JavaScript que realiza las actualizaciones, sería similar al siguiente:
<script> var listaTareas = [] var app = document.querySelector("#lista") const update = () => { app.innerHTML = listaTareas.map(({ titulo }) => `<li>${titulo}</li>`).join("") } var form = document.querySelector("#form") form.addEventListener("submit", (ev) => { ev.preventDefault() listaTareas.push({ titulo: document.querySelector("[name='titulo']").value }) update() }) update() </script>
Como puedes observar, parte del código se ha trasladado desde la aplicación que se ejecutaba completamente en el servidor: la que calcula el código HTML que debe insertarse en la lista. Lo demás consiste en una función inscrita como manejador del evento “submit” del formulario, que simplemente actualiza la estructura de datos de la lista de tareas e invoca la actualización del elemento.
Si ahora abrimos esta página en un navegador, podremos agregar nuevas tareas de forma instantánea, sin esperar a que el servidor dé una respuesta o que la página se refresque para ver la lista actualizada. Lógicamente, habría que incluir en el guion JavaScript una petición al servidor para almacenar los nuevos datos, pero esta se puede realizar de forma asíncrona sin que afecte a la rapidez de la interfaz.
Componente React equivalente
Aunque no conozcas todavía la forma de programar componentes con React, vamos a mostrar cuál sería el código con el que obtendríamos una página similar a la que hemos compuesto en JavaScript puro.
Se han omitido los detalles más técnicos en el siguiente listado de código, pero puedes observar cómo en React se integran los elementos de interfaz con la lógica de generación de elementos a medida de la información que queramos mostrar:
const Lista = () => { // se han omitido los manejadores de eventos return <> <ul> {tareas.map(({ titulo }) => <li>{titulo}</li>)} </ul> <form id="form" onSubmit={handleSubmit}> <input type="text" name="titulo" placeholder="Nueva tarea" value={nueva} onChange={handleNueva} autoFocus /> <input type="submit" value="Añadir"/> </form> </> }
De forma similar a como podríamos hacer en el lado del servidor, escribimos una “plantilla” donde para cada tarea se añadirá un elemento de lista. Lo interesante está en que no es necesario escribir ningún código para que esta plantilla se mantenga actualizada, es decir, React se encarga de monitorizar los cambios en la información y de modificar en la página web únicamente los elementos que sean estrictamente necesarios.
Otra ventaja de escribir la misma aplicación en React es que se hace de forma declarativa en lugar de imperativa. Donde en JavaScript puro debíamos asignar un nuevo valor al innerHTML de la lista, en React simplemente hemos descrito el contenido de la página y la biblioteca se encargará de construir y actualizar los nodos del DOM cuando sea necesario. De esta manera, podemos elevar la complejidad de nuestra aplicación sin necesidad de estar controlando en cada momento los elementos de la página que deben cambiar.
