Renderizado
El renderizado es el proceso de generar una imagen a partir de un modelo o modelos. Siendo el modelo una “descripción de una escena”, y el renderizado el “proceso de generar una imagen” a partir de esa descripción.
En una aplicación web, el modelo es el DOM (Document Object Model) y el renderizado es el proceso que realiza el navegador para mostrar la página web.
Este proceso de renderizado se realiza cada vez que se produce un cambio en el DOM, ya sea por una modificación en el código o por una interacción del usuario.
Típicamente, el renderizado del DOM es un proceso costoso, por lo que es importante que se realice de la forma más eficiente posible.
El proceso de renderizado del DOM le indica al navegador cómo debe mostrar la página web. En este proceso, podemos destacar dos instancias importantes, la alteración del estado de los elementos del DOM (esto ocurre en memoria) y la actualización de la interfaz gráfica (esto directamente en el navegador o el cliente que está visualizando la aplicación).
Proceso de renderizado del DOM
- Recorrer el DOM: El navegador recorre el DOM para identificar los elementos que han cambiado.
- Calcular el estilo: El navegador calcula el estilo de los elementos que han cambiado.
- Determinar la posición: El navegador determina la posición de los elementos que han cambiado.
- Renderizar los elementos: El navegador renderiza los elementos que han cambiado.
Gráficamente, podemos interpretar que el navegador será un intermediario entre el DOM y el cliente, analizando los cambios que se han producido.
Por supuesto, tradicionalmente, este proceso de renderizado redibuja toda la página web, lo que puede ser ineficiente en ciertos casos.
Supongamos que tenemos una aplicación web que muestra una lista de tareas. Cada vez que se añade una nueva tarea, se añade un nuevo elemento al DOM. Si agregamos 100 tareas, el navegador deberá renderizar 100 veces el DOM, no sólo se renderizará puntualmente el nuevo elemento, sino que ocurriá en toda la página.
Si el proceso de renderizado es costoso, esto puede afectar al rendimiento de la aplicación, y puede provocar que la aplicación se vuelva lenta o incluso que se bloquee, como suele ocurrir con cieras aplicaciones web.
Para evitar esto, React utiliza un proceso de renderizado distinto al que utiliza el navegador, para lo cual utiliza un modelo de renderizado llamado Virtual DOM.
Virtual DOM
Debido al alto costo de renderizado del DOM, React diseñó un modelo de renderizado alternativo en el cual se genera una copia del DOM en memoria, llamado Virtual DOM.
El Virtual DOM se utiliza para calcular los cambios que se deben realizar en el DOM. Para realizar este cálculo, React utiliza un algoritmo de comparación para identificar las diferencias entre el Virtual DOM con el DOM, y determina los cambios que se deben realizar en el DOM para que este coincida con el Virtual DOM.
En otras palabras, los cambios que deberían impactar en el DOM se realizan primero en el Virtual DOM.
Así, el Virtual DOM se convierte en la representación que se espera que tenga el DOM, para el cual sólo los elementos que hayan cambiado se renderizarán de nuevo.
Por supuesto, este proceso conlleva un costo adicional, ya que se debe mantener sincronizado el Virtual DOM con el DOM. Sin embargo, en la gran mayoría de los casos, el impacto de estas comparaciones es exponencialmente menor al que tendría el continuo renderizado del DOM. Evidentemente, el renderizado de elementos en una interfaz gráfica es una tarea mucho más costosa que comparar dos objetos en memoria.
Por ejemplo, si tenemos una lista de elementos.
1<ul>2 <li>Elemento 1</li>3 <li>Elemento 2</li>4 <li>Elemento 3</li>5</ul>
Si quisieramos realizar dos operaciones sobre esta lista:
- Eliminar el elemento 3
- Añadir un elemento 4
El proceso de renderizado del DOM sería el siguiente:
- Eliminar el elemento 3
- Renderizar la lista
- Añadir el elemento 4
- Renderizar la lista
Claro está, que no podríamos ver el resultado de la eliminación del elemento 3 con mucha facilidad pues es una operación que se realiza muy rápido, pero el renderizado de la lista ocurrirá igualmente.
1<ul>2 <li>Elemento 1</li>3 <li>Elemento 2</li>4</ul>
Consecuentemente, el navegador tendrá que realizar el mismo proceso de renderizado al añadir el elemento 4.
1<ul>2 <li>Elemento 1</li>3 <li>Elemento 2</li>4 <li>Elemento 4</li>5</ul>
En cambio, el proceso de renderizado del Virtual DOM sería bastante distinto. En este caso, las operaciones que mencionamos anteriormente se realizarían sobre el Virtual DOM, y como resultado, el Virtual DOM tendría la forma del estado final del DOM.
1<ul>2 <li>Elemento 1</li>3 <li>Elemento 2</li>4 <li>Elemento 4</li>5</ul>
En este punto se realizaría la comparación entre el Virtual DOM y el DOM, y React se percataría que la cantidad de elementos <li>
es la misma, y que la modificación más eficiente sería reemplazar el texto del elemento 3 por el texto del elemento 4. Como resultado, el DOM se modificaría para que coincida con el Virtual DOM.
Aun con todo esto en mente, queremos resaltar donde radica la ventaja de emplear el Virtual DOM.
Computacionalmente, renderizar una página web es una tarea costosa. Generar elementos gráficos, calcular su posición, aplicar estilos, etc. son tareas que requieren de muchos recursos.
En contraposición, comparar dos objetos (en este caso el Virtual DOM y el DOM) es una tarea ínfima. Esto sin tomar en cuenta que el proceso puede reducirse aún más si tomamos en cuenta que una aplicación React divide el DOM en componentes, y que el proceso de renderizado se realiza de forma independiente para cada componente.
En general, veremos que el desarrollo de aplicaciones a gran escala se verán beneficiadas por este modelo de renderizado.
React DOM
Con las consideraciones mencionadas previamente, podemos entender que React manipula el DOM de forma distinta, accediendo a una copia en memoria del mismo. Para realizar esta tarea, React utiliza una librería llamada React DOM.
En retrospectiva, podemos recordar que ya hemos visto esta librería al crear una aplicación con Vite, en el archivo main.jsx
.
1import React from "react";2import ReactDOM from "react-dom/client";3import App from "./components/App.jsx";4
5ReactDOM.createRoot(document.getElementById("root")).render(<App />);
ReactDOM
nos permite crear y manipular el Virtual DOM empleando el método createRoot
, el cual que recibe como argumento un elemento del DOM sobre el cual se creará el Virtual DOM. Es decir, que este archivo es el punto de entrada de la aplicación React, y es el encargado de crear el Virtual DOM a partir de un elemento existente en una plantilla HTML. Esta plantilla HTML también es creada por Vite, el archivo index.html
.
1<!doctype html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <link rel="icon" type="image/svg+xml" href="/vite.svg" />6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />7 <title>React Color Chooser</title>8 </head>9 <body>10 <div id="root"></div>11 <script type="module" src="/src/main.jsx"></script>12 </body>13</html>
Vite establece un elemento div
con el id = "root"
sobre el cual se creará el Virtual DOM, y por ende, el elemento sobre el cual se realizará el renderizado de la aplicación. Todo elemento que se cree en la aplicación, se creará como hijo de este elemento. Lógicamente, cualquier elemento que se encuentre al mismo o mayor nivel que este elemento, no será parte de la aplicación React.
Renderizado condicional
Ahora que conocemos el proceso de renderizado de React, podemos analizar otras maneras de renderizar elementos.
En ocasiones, los componentes que definimos pueden generar diferentes elementos dependiendo de ciertas condiciones. Por ejemplo, supongamos que tenemos una lista de compras, y que algunos elementos de la lista pueden estar marcados como comprados. Para representar esto, podemos definir dos componentes, uno para representar un elemento de la lista (ListItem
), y otro para representar la lista de compras (ShoppingList
).
1function ShoppingList() {2 return (3 <ul>4 <ListItem name="Leche" isBought={true} />5 <ListItem name="Huevos" isBought={false} />6 <ListItem name="Pan" isBought={false} />7 </ul>8 );9}
Teniendo en cuenta que el componente ListItem
recibe como propiedades el nombre del elemento (name
) y un valor booleano que indica si el elemento está comprado (isBought
), podríamos definir el componente empleando una estructura condicional if-else
y devolver un elemento li
con formato distinto dependiendo del valor de la propiedad isBought
.
1function ListItem(props) {2 if (props.isBought) {3 return <li> <del> {props.name} </del> </li>;4 } else {5 return <li> {props.name} </li>;6 }7}
De esta manera, si el elemento está comprado, se mostrará con una línea tachada, y si no, se mostrará sin ninguna modificación. Este tipo de operación en JSX se conoce como exclusión conditional.
1<ul>2 <li><del>Leche</del></li>3 <li>Huevos</li>4 <li>Pan</li>5</ul>
Operador ternario
Sin embargo, esta no es la única manera de realizar la exclusión condicional. Podemos emplear el operador ternario (u operador condicional) para realizar la misma operación. Cabe resaltar que este operador forma parte de la sintaxis de JavaScript, y no necesariamente de JSX o React.
La sintaxis del operador ternario es la siguiente:
1condición ? expresión1 : expresión2
Este operador puede interpretarse de la siguiente manera:
Si la condición es verdadera (evalúa a
true
), entonces devuelve la expresión1, de lo contrario, devuelve la expresión2.
Por lo tanto, podemos refactorizar el componente ListItem
de la siguiente manera:
1function ListItem(props) {2 return (3 <li> {props.isBought ? <del> {props.name} </del> : props.name} </li>4 );5}
De esta manera, si la propiedad isBought
es verdadera, se agregará un elemento hijo del
al elemento li
, y si no, se mostrará el nombre del elemento sin ninguna modificación.
Como podemos apreciar, el operador ternario nos permite realizar la exclusión condicional de una manera más concisa y legible. Siempre y cuando la expresión a retornar no sea muy compleja, podemos emplear el operador ternario para realizar la exclusión condicional. El principal beneficio de emplear este operador es que nuestro código se apega más al principio de DRY (Don’t Repeat Yourself), pues evitamos repetir código innecesariamente.
En este caso, la expresión a retornar es un elemento que única y exclusivamente depende del valor de la propiedad isBought
. Además, difiere en un solo elemento (del
), por lo que podemos emplear el operador ternario sin ningún problema.
Cabe mencionar que este operador puede emplearse en varias lineas, siempre y cuando se encierre entre paréntesis. De esta manera podemos devolver elementos más complejos sin sacrificar la legibilidad del código. Por ejemplo, supongamos que qusiéramos encerar el nombre del producto en un elemento span
para darle un estilo distinto por medio de CSS.
1function ListItem(props) {2 return (3 <li>4 {props.isBought ? (5 <del> {props.name} </del>6 ) : (7 <span className="product-name"> {props.name} </span>8 )}9 </li>10 );11}
Operador lógico AND
Otra manera de realizar la exclusión condicional es empleando el operador lógico AND (&&
) de JavaScript. A menudo surge la necesidad de renderizar elementos condicionalmente cuando una condición es verdadera, y no renderizar nada en caso contrario.
Por ejemplo, supongamos que tenemos un componente que recibe como propiedad un valor booleano que indica si el usuario está conectado (isLogged
), y que queremos mostrar un mensaje de bienvenida si el usuario está conectado.
1function WelcomeMessage(props) {2 if (props.isLogged) {3 return <h1> Bienvenido! </h1>;4 } else {5 return null;6 }7}
En este caso, si el usuario está conectado, se mostrará el mensaje de bienvenida, y si no, no se mostrará nada. Para este caso, podemos emplear el operador lógico AND para realizar la exclusión condicional.
1function WelcomeMessage(props) {2 return props.isLogged && <h1> Bienvenido! </h1>;3}
Una expresión Javascript &&
devuelve el valor de su lado derecho (en nuestro caso, la marca de verificación) si el lado izquierdo (nuestra condición) es true
. Pero si la condición es false
, toda la expresión se convierte en false
, y como mencionamos en ocasiones anteriores, React no renderiza elementos al recibir expresiones false
, null
o undefined
.
Por lo tanto, si la condición es verdadera, se devolverá el elemento h1
, y si no, se devolverá false
, y React no renderizará nada.
Atención: jamás emplees números como condiciones en JSX, pues React renderizará el número
0
en lugar defalse
. Sólo emplea expresiones booleanas.
Por ejemplo, si quisiéramos evaluar la condición un contador de mensajes no leídos, y mostrar un mensaje si hay mensajes no leídos, podríamos emplear el operador lógico AND de la siguiente manera:
1messagesCount && <h1> Tienes {messagesCount} mensajes no leídos </h1>
En Javascript, si el valor de messagesCount
es 0
, la expresión se evaluará como false
. Sin embargo, React no es capaz de interpretar esto, y renderizará el número 0
en lugar del valor booleano false
. Por lo cual, renderizará 0
si el valor de messagesCount
es 0
en lugar de evitar el proceso de renderizado.
Por lo tanto, si queremos evitar el renderizado de 0
, debemos siempre emplear expresiones booleanas como condiciones.
1messagesCount > 0 && <h1> Tienes {messagesCount} mensajes no leídos </h1>
Renderizado de listas
Otra tarea común en React es el renderizado de listas. En ocasiones, los componentes que definimos son similares a una colección de datos. Para estos casos, los métodos de los arreglos de Javascript son de gran utilidad.
Analizaremos dos métodos de los arreglos de Javascript que nos permiten renderizar listas de elementos, el método map
y el método filter
.
Método map
Volviendo al ejemplo de la lista de compras, analicemos ahora el componente ShoppingList
. Hasta el momento, hemos definido los elementos de la lista de forma estática, pero en la mayoría de los casos, los elementos de una lista son dinámicos, y dependen de datos persistentes como los almacenados en un archivo, una base de datos o una API.
Para este caso, podemos emplear el siguiente archivo shopping-list.json
para almacenar los datos de la lista de compras.
1[2 {3 "name": "Leche",4 "isBought": true5 },6 {7 "name": "Huevos",8 "isBought": false9 },10 {11 "name": "Pan",12 "isBought": false13 }14]
Para emplear estos datos en nuestro componente, podemos importarlos y emplearlos como si fueran datos estáticos.
1import data from "../<path>/shopping-list.json";2
3function ShoppingList() {4 return (5 <ul>6 <ListItem name={data[0].name} isBought={data[0].isBought} />7 <ListItem name={data[1].name} isBought={data[1].isBought} />8 <ListItem name={data[2].name} isBought={data[2].isBought} />9 </ul>10 );11}
Ahora bien, si quisiéramos agregar un nuevo elemento a la lista, tendríamos que agregar un nuevo elemento a la lista de datos, y agregar un nuevo elemento al componente ShoppingList
. Esto es inviable en un escenario real, pues la lista de datos puede ser muy extensa, y supondría mantenimiento constante para un cambio tan simple.
Para evitar esto, podemos emplear el método map
sobre un arreglo con los datos del archivo JSON, y devolver un elemento ListItem
por cada elemento del arreglo.
1import data from "../<path>/shopping-list.json";2
3function ShoppingList() {4 return (5 <ul>6 {data.map((item) => (7 <ListItem name={item.name} isBought={item.isBought} />8 ))}9 </ul>10 );11}
Otro hecho importante en a destacar en este ejemplo, es que todos los componentes ListItem
que se creen, tendrán las mismas propiedades. Aunque en este caso, sólo indicamos dos atributos (name
e isBought
), en un escenario real, es posible que un componente tenga muchas más propiedades. Por lo tanto, es importante que todos los componentes tengan las mismas propiedades, y que estas propiedades sean consistentes. Al mantener esta consistencia, podemos utlizar el operador de propagación (...
) para pasar las propiedades del elemento como argumentos del componente ListItem
.
1import data from "../<path>/shopping-list.json";2
3function ShoppingList() {4 return (5 <ul>6 {data.map((item) => (7 <ListItem {...item} />8 ))}9 </ul>10 );11}
De esta manera, podemos pasar todas las propiedades del elemento como argumentos del componente ListItem
, y el componente ListItem
las recibirá como propiedades.
Método filter
Este método, como su nombre lo indica, nos permite filtrar los elementos de un arreglo. Este método recibe como argumento una función que devuelve un valor booleano, y devuelve un nuevo arreglo con los elementos que cumplan con la condición (es decir, que filter
es una función de orden superior).
Para ejemplificar esto, consideremos que queremos realizar un comportamiento distinto al que hemos definido hasta ahora. En este caso, contamos con un nuevo archivo important-shopping-list.json
que contiene un lista de compras donde sus elementos pueden ser importantes o no. Además, agregaremos un nuevo dato para identificar cada objeto, un id
.
1[2 {3 "id": 1,4 "name": "Leche",5 "isBought": true,6 "isImportant": false7 },8 {9 "id": 2,10 "name": "Huevos",11 "isBought": false,12 "isImportant": true13 },14 {15 "id": 3,16 "name": "Pan",17 "isBought": false,18 "isImportant": false19 }20]
Ahora, podemos emplear el método filter
para filtrar los productos importantes. Además, agregaremos una propiedad al componente ShoppingList
para indicar que se trata de una lista de productos importantes.
En caso de que la lista sea de productos importantes, se filtrarán los productos importantes, renderizando una lista mediante el método filter
y map
, y en caso contrario, se renderizará la lista completa utilizando únicamente el método map
.
1import data from "../<path>/shopping-list.json";2import importantData from "../<path>/important-shopping-list.json";3
4function ShoppingList(props) {5 return (6 <ul>7 {props.isImportant8 ? importantData9 .filter((item) => item.isImportant)10 .map((item) => <ListItem {...item} />)11 : data.map((item) => <ListItem {...item} />)}12 </ul>13 );14}
Resaltamos el hecho de que el método filter
devuelve un nuevo arreglo, y que este método no modifica el arreglo original. Por otro lado, debemos usar filter
en conjunto con map
para renderizar los elementos filtrados, pues filter
devuelve un arreglo con los datos, pero no genera los elementos React que necesitamos (es decir, no genera los elementos ListItem
).
Atención: las funciones flecha empleadas en los métodos
map
yfilter
devuelven implícitamente una expresión, por lo cual no es necesario emplear la palabra clavereturn
para devolver cada elemento.
Es decir, las siguientes sentencias son equivalentes:
1data.map((item) => <ListItem {...item} />);2
3data.map((item) => {4 return <ListItem {...item} />;5});
Claves
Por último, es de suma importancia que los elementos de una lista tengan una única propiedad llamada key
. Esta propiedad es necesaria para que React pueda identificar cada elemento de la lista de forma única, y así poder realizar el proceso de renderizado de forma eficiente. De otra manera, los elementos de la lista podrían compartir referencias con otro ya existente y serán indistinguibles.
Para realizar esto, podemos emplear la propiedad key
de los componentes ListItem
que creamos y asignarle el valor del atributo id
de cada objeto en el arreglo.
1import data from "../<path>/shopping-list.json";2import importantData from "../<path>/important-shopping-list.json";3
4function ShoppingList(props) {5 return (6 <ul>7 {props.isImportant8 ? importantData9 .filter((item) => item.isImportant)10 .map((item) => <ListItem key={item.id} {...item} />)11 : data.map((item) => <ListItem key={item.id} {...item} />)}12 </ul>13 );14}
Las keys le indican a React que objeto del array corresponde a cada componente, para así poder emparejarlo más tarde. Esto se vuelve más importante si los objetos de tus arrays se pueden mover (p. ej. debido a un ordenamiento), insertar, o eliminar. Si React no tiene una key, React no sabrá que elemento ha cambiado, y por lo tanto, no podrá actualizarlo de forma eficiente.
Como recomendación, deberíamos siempre tener un valor en las colecciones de datos que pueda servir como identificador único, la clave primaria de una tabla en una base de datos, por ejemplo.
Por último, debemos mencionar que si omitimos la propiedad key
, React empleará el índice del elemento en el arreglo como clave. No obstante, si ocurre un cambio en el arreglo (por ejemplo, si se inserta un nuevo elemento), React podría perder cierta información y no renderizar ciertos elementos o bien ordenarlos arbitrariamente.