Capítulo 1. Lo básico
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Empecemos con un descargo de responsabilidad: React está hecho para que lo use todo el mundo. De hecho, ¡podrías ir por la vida sin haber leído nunca este libro y seguir utilizando React sin problemas! Este libro profundiza mucho más en React para quienes sentimos curiosidad por su mecanismo subyacente, patrones avanzados y buenas prácticas . Se presta más a conocer cómo funciona React que a aprender a usarlo. Hay muchos otros libros escritos con la intención de enseñar a la gente a utilizar React como usuario final. En cambio, este libro te ayudará a entender React al nivel de un autor de bibliotecas o frameworks en lugar de un usuario final. Siguiendo con ese tema, vamos a sumergirnos juntos en profundidad, empezando por arriba: los temas de nivel más alto y básico. Empezaremos con los fundamentos de React, y luego profundizaremos cada vez más en los detalles de cómo funciona React.
En este capítulo hablaremos de por qué existe React, cómo funciona y qué problemas resuelve. Cubriremos su inspiración y diseño iniciales, y lo seguiremos desde sus humildes comienzos en Facebook hasta la solución predominante que es hoy. Este capítulo es un poco meta (no es un juego de palabras), porque es importante entender el contexto de React antes de sumergirnos en los detalles.
¿Por qué existe React?
La respuesta en una palabra es: actualizaciones. En los primeros días de la web, teníamos muchas páginas estáticas. Rellenábamos formularios, pulsábamos Enviar y se cargaba una página completamente nueva. Esto estuvo bien durante un tiempo, pero con el tiempo las experiencias web evolucionaron significativamente en cuanto a capacidades. A medida que crecían las capacidades, también lo hacía nuestro deseo de experiencias de usuario superiores en la web. Queríamos poder ver que las cosas se actualizaban instantáneamente sin tener que esperar a que se renderizara y cargara una nueva página. Queríamos que la web y sus páginas parecieran más ágiles e "instantáneas". El problema era que estas actualizaciones instantáneas eran bastante difíciles de hacer a escala por varias razones:
- Rendimiento
-
Actualizar las páginas web a menudo provocaba cuellos de botella en el rendimientoporque éramos propensos a realizar trabajos que hacían que los navegadores recalcularan el diseño de una página (lo que se denomina reflujo) y volvieran a pintarla.
- Fiabilidad
-
Hacer un seguimiento del estado y asegurarnos de que el estado era coherente en una experiencia web enriquecida era difícil porque teníamos que hacer un seguimiento del estado en varios lugares y asegurarnos de que el estado era coherente en todos esos lugares. Esto era especialmente difícil cuando había varias personas trabajando en el mismo código base.
- Seguridad
-
Teníamos que asegurarnos de que saneaba todo el HTML y JavaScript que inyectábamos en la página para evitar exploits como el cross-site scripting (XSS) y el cross-site request forgery (CSRF).
Para comprender y apreciar plenamente cómo React nos resuelve estos problemas, tenemos que entender el contexto en el que se creó React y el mundo sin React o anterior a React. Hagámoslo ahora.
El mundo antes de reaccionar
Estos eran algunos de los grandes problemas para los que creábamos aplicaciones web antes de React. Teníamos que averiguar cómo hacer que nuestras aplicaciones fueran rápidas e instantáneas, pero también escalables a millones de usuarios y que funcionaran de forma fiable y segura. Por ejemplo, consideremos la pulsación de un botón: cuando un usuario pulsa un botón, queremos actualizar la interfaz de usuario para reflejar que se ha pulsado el botón. Tendremos que considerar al menos cuatro estados diferentes en los que puede estar la interfaz de usuario:
- Pre-clic
-
El botón está en su estado por defecto y no ha sido pulsado.
- Pulsado pero pendiente
-
Se ha pulsado el botón, pero la acción que se supone que debe realizar el botón aún no se ha completado.
- Clic y éxito
-
Se ha hecho clic en el botón, y la acción que se supone que debe realizar el botón se ha completado. A partir de aquí, puede que queramos revertir el botón a su estado anterior al clic, o puede que queramos que el botón cambie de color (verde) para indicar el éxito.
- Pulsado y fallado
-
Se ha hecho clic en el botón, pero la acción que se supone que debe realizar el botón ha fallado. A partir de aquí, puede que queramos revertir el botón a su estado anterior al clic, o puede que queramos que el botón cambie de color (rojo) para indicar el fallo.
Una vez que tengamos estos estados, tenemos que averiguar cómo actualizar la interfaz de usuario para que los refleje. A menudo, actualizar la interfaz de usuariorequeriría los siguientes pasos:
-
Encuentra el botón en el entorno anfitrión (a menudo el navegador) utilizando algún tipo de API localizadora de elementos, como
document.querySelector
odocument.getElementById
. -
Añade escuchadores de eventos al botón para escuchar los eventos de clic.
-
Realiza cualquier actualización de estado en respuesta a eventos.
-
Cuando el botón abandone la página, elimina los escuchadores de eventos y limpia cualquier estado.
Este es un ejemplo sencillo, pero es bueno para empezar. Supongamos que tenemos un botón con la etiqueta "Me gusta", y cuando un usuario hace clic en él, queremos actualizar el botón a "Me gusta". ¿Cómo lo hacemos? Para empezar, tendríamos un elemento HTML:
<
button
>
Like</
button
>
Necesitaríamos alguna forma de hacer referencia a este botón con JavaScript, así que le daríamos un atributo id
:
<
button
id
=
"likeButton"
>
Like</
button
>
¡Estupendo! Ahora que hay un id
, JavaScript puede trabajar con él para hacerlo interactivo. Podemos obtener una referencia al botón utilizandodocument.getElementById
, y luego añadiremos un receptor de eventos al botón para escuchar los eventos de clic:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
// do something
});
Ahora que tenemos un receptor de eventos, podemos hacer algo cuando se pulse el botón. Digamos que queremos actualizar el botón para que tenga la etiqueta "Me gusta" cuando se pulse. Podemos hacerlo actualizando el contenido del texto del botón:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
likeButton
.
textContent
=
"Liked"
;
});
¡Genial! Ahora tenemos un botón que dice "Me gusta" y, cuando se pulsa, dice "Me gusta". El problema aquí es que no podemos "no gustar" cosas. Arreglemos eso y actualicemos el botón para que vuelva a decir "Me gusta" si se pulsa en su estado "Me gusta". Tendríamos que añadir algún estado al botón para saber si se ha pulsado o no. Podemos hacerlo añadiendo un atributo data-liked
al botón:
<
button
id
=
"likeButton"
data-liked
=
"false"
>
Like</
button
>
Ahora que tenemos este atributo, podemos utilizarlo para saber si se ha pulsado o no el botón. Podemos actualizar el contenido del texto del botón en función del valor de este atributo:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
});
Espera, ¡pero sólo estamos cambiando el textContent
del botón! En realidad no estamos guardando el estado "Me gusta" en una base de datos. Normalmente, para hacer esto tendríamos que comunicarnos a través de la red, así:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
var
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
var
xhr
=
new
XMLHttpRequest
();
xhr
.
open
(
"POST"
,
"/like"
,
true
);
xhr
.
setRequestHeader
(
"Content-Type"
,
"application/json;charset=UTF-8"
);
xhr
.
onload
=
function
()
{
if
(
xhr
.
status
>=
200
&&
xhr
.
status
<
400
)
{
// Success!
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
}
else
{
// We reached our target server, but it returned an error
console
.
error
(
"Server returned an error:"
,
xhr
.
statusText
);
}
};
xhr
.
onerror
=
function
()
{
// There was a connection error of some sort
console
.
error
(
"Network error."
);
};
xhr
.
send
(
JSON
.
stringify
({
liked
:
!
liked
}));
});
Por supuesto, estamos utilizando XMLHttpRequest
y var
para ser relevantes en el tiempo. React se lanzó como software de código abierto en 2013, y la API más comúnfetch
se introdujo en 2015. Entre XMLHttpRequest
yfetch
, teníamos jQuery, que a menudo abstraía parte de la complejidad con primitivas como $.ajax()
, $.post()
, etc.
Si escribiéramos esto hoy, se parecería más a esto:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
}).
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
});
});
Sin divagar demasiado, la cuestión ahora es que nos estamos comunicando a través de la red, pero ¿qué ocurre si falla la solicitud de red? Tendríamos que actualizar el contenido del texto del botón para reflejar el fallo. Podemos hacerlo añadiendo un atributo data-failed
al botón:
<
button
id
=
"likeButton"
data-liked
=
"false"
data-failed
=
"false"
>
Like</
button
>
Ahora podemos actualizar el contenido del texto del botón en función del valor de este atributo:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
})
.
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
})
.
catch
(()
=>
{
likeButton
.
setAttribute
(
"data-failed"
,
true
);
likeButton
.
textContent
=
"Failed"
;
});
});
Hay un caso más que tratar: el proceso de "gustar" actualmente una cosa. Es decir, el estado pendiente. Para modelar esto en código, estableceríamos otro atributo más en el botón para el estado pendiente, añadiendodata-pending
, de este modo:
<
button
id
=
"likeButton"
data-pending
=
"false"
data-liked
=
"false"
data-failed
=
"false"
>
Like</
button
>
Ahora podemos desactivar el botón si hay una solicitud de red en curso, para que los clics múltiples no pongan en cola las solicitudes de red y provoquen extrañas condiciones de carrera y sobrecarga del servidor. Podemos hacerlo así
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
const
isPending
=
likeButton
.
getAttribute
(
"data-pending"
)
===
"true"
;
likeButton
.
setAttribute
(
"data-pending"
,
"true"
);
likeButton
.
setAttribute
(
"disabled"
,
"disabled"
);
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
})
.
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
likeButton
.
setAttribute
(
"disabled"
,
null
);
})
.
catch
(()
=>
{
likeButton
.
setAttribute
(
"data-failed"
,
"true"
);
likeButton
.
textContent
=
"Failed"
;
})
.
finally
(()
=>
{
likeButton
.
setAttribute
(
"data-pending"
,
"false"
);
});
});
También podemos utilizar técnicas potentes como el debouncing y el throttling para evitar que los usuarios realicen acciones redundantes o repetitivas.
Nota
Como apunte rápido, mencionamos debouncing y throttling. Para que quede claro, la supresión de rebotes retrasa la ejecución de una función hasta que haya transcurrido un tiempo determinado desde el último evento desencadenante (por ejemplo, espera a que los usuarios dejen de escribir para procesar la entrada), y el estrangulamiento limita la ejecución de una función a un máximo de una vez cada intervalo de tiempo determinado, asegurándose de que no se ejecuta con demasiada frecuencia (por ejemplo, procesa eventos de desplazamiento a intervalos determinados). Ambas técnicas optimizan el rendimiento controlando la frecuencia de ejecución de las funciones.
Bien, ahora nuestro botón es bastante robusto y puede manejar varios estados, pero aún quedan algunas preguntas:
-
¿Es realmente necesario
data-pending
? ¿No podemos simplemente comprobar si el botón está desactivado? Probablemente no, porque un botón desactivado podría estarlo por otras razones, como que el usuario no haya iniciado sesión o no tenga permiso para pulsar el botón. -
¿Tendría más sentido tener un atributo
data-state
, dondedata-state
puede ser uno depending
,liked
, ounliked
, en lugar de tantos otros atributos de datos? Probablemente, pero entonces tendríamos que añadir un gran bloque de código switch/case o similar para manejar cada caso. En última instancia, el volumen de código para manejar ambos enfoques es incomparable: seguimos teniendo complejidad y verbosidad de cualquier forma. -
¿Cómo probamos este botón de forma aislada? ¿Podemos?
-
¿Por qué tenemos el botón escrito inicialmente en HTML , y después trabajamos con él en JavaScript? ¿No sería mejor si pudiéramos crear el botón en JavaScript con
document.createElement('button')
y luego condocument.appendChild(likeButton)
? Esto facilitaría las pruebas y haría que el código fuera más autónomo, pero entonces tendríamos que seguir la pista de su padre si su padre no esdocument
. De hecho, tendríamos que hacer un seguimiento de todos los padres de la página.
React nos ayuda a resolver algunos de estos problemas, pero no todos: por ejemplo, la cuestión de cómo dividir el estado en banderas separadas (isPending
, hasFailed
, etc.) o en una única variable de estado (comostate
) es una pregunta que React no responde por nosotros. Es una pregunta que tenemos que responder nosotros mismos. Sin embargo, React sí nos ayuda a resolver el problema de la escala: crear muchos botones que deben ser interactivos y actualizar la interfaz de usuario en respuesta a eventos de una forma mínima y eficiente, y hacerlo de una manera comprobable, reproducible, declarativa, eficaz, predecible y fiable.
Además, React nos ayuda a hacer que el estado sea mucho más predecible al poseer por completo el estado de la interfaz de usuario y renderizar en función de ese estado. Esto contrasta fuertemente con el hecho de que el estado pertenezca al navegador y sea operado por él, cuyo estado puede ser muy poco fiable debido a una serie de factores como otros scripts del lado del cliente que se ejecutan en la página, extensiones del navegador, limitaciones del dispositivo y muchas más variables.
Nuestro ejemplo con el botón Me gusta es un ejemplo muy simple, pero es bueno para empezar. Hasta ahora, hemos visto cómo podemos utilizar JavaScript para hacer que un botón sea interactivo, pero se trata de un proceso muy manual si queremos hacerlo bien: tenemos que encontrar el botón en el navegador, añadir un receptor de eventos, actualizar el contenido del texto del botón y tener en cuenta innumerables casos de perímetro. Esto es mucho trabajo, y no es muy escalable. ¿Y si tuviéramos muchos botones en la página? ¿Y si tuviéramos muchos botones que tuvieran que ser interactivos? ¿Y si tuviéramos muchos botones que tuvieran que ser interactivos y tuviéramos que actualizar la interfaz de usuario en respuesta a eventos? ¿Utilizaríamos la delegación de eventos (o burbujeo de eventos) y adjuntaríamos un escuchador de eventos al document
superior? ¿O deberíamos adjuntar escuchadores de eventos a cada botón?
Como se indica en el Prefacio, este libro asume que entendemos satisfactoriamente esta afirmación: los navegadores renderizan páginas web. Las páginas web son documentos HTML a los que se aplica estilo mediante CSS y que se hacen interactivos con JavaScript. Esto ha funcionado muy bien durante décadas y sigue haciéndolo, pero construir aplicaciones web modernas que pretendan dar servicio a una cantidad significativa (piensa en millones) de usuarios con estas tecnologías requiere una buena cantidad de abstracción para hacerlo de forma segura y fiable con la menor posibilidad de error posible. Por desgracia, basándonos en el ejemplo del botón "Me gusta" que hemos estado explorando, está claro que vamos a necesitar ayuda con esto.
Consideremos otro ejemplo un poco más complejo que nuestro botón Me gusta. Empezaremos con un ejemplo sencillo: una lista de elementos. Supongamos que tenemos una lista de elementos y queremos añadir un nuevo elemento a la lista. Podríamos hacerlo con un formulario HTML parecido a éste:
<
ul
id
=
"list-parent"
></
ul
>
<
form
id
=
"add-item-form"
action
=
"/api/add-item"
method
=
"POST"
>
<
input
type
=
"text"
id
=
"new-list-item-label"
/>
<
button
type
=
"submit"
>
Add Item</
button
>
</
form
>
JavaScript nos da acceso a las API del Modelo de Objetos del Documento (DOM). Para los que no lo sepan, el DOM es un modelo en memoria de la estructura del documento de una página web: es un árbol de objetos que representa los elementos de tu página, ofreciéndote formas de interactuar con ellos mediante JavaScript. El problema es que los DOM de los dispositivos de los usuarios son como un planeta alienígena: no tenemos forma de saber qué navegadores están utilizando, en qué condiciones de red y en qué sistemas operativos (SO) están trabajando. ¿Cuál es el resultado? Tenemos que escribir código que sea resistente a todos estos factores.
Como ya hemos comentado, el estado de la aplicación se vuelve bastante difícil de predecir cuando se actualiza sin algún tipo de mecanismo de reconciliación de estados que lleve la cuenta de las cosas. Para continuar con nuestro ejemplo de la lista, consideremos algo de código JavaScript para añadir un nuevo elemento a la lista:
(
function
myApp
()
{
var
listItems
=
[
"I love"
,
"React"
,
"and"
,
"TypeScript"
];
var
parentList
=
document
.
getElementById
(
"list-parent"
);
var
addForm
=
document
.
getElementById
(
"add-item-form"
);
var
newListItemLabel
=
document
.
getElementById
(
"new-list-item-label"
);
addForm
.
onsubmit
=
function
(
event
)
{
event
.
preventDefault
();
listItems
.
push
(
newListItemLabel
.
value
);
renderListItems
();
};
function
renderListItems
()
{
for
(
i
=
0
;
i
<
listItems
.
length
;
i
++
)
{
var
el
=
document
.
createElement
(
"li"
);
el
.
textContent
=
listItems
[
i
];
parentList
.
appendChild
(
el
);
}
}
renderListItems
();
})();
Este fragmento de código está escrito para parecerse lo más posible a las primeras aplicaciones web. ¿Por qué se estropea con el tiempo? Principalmente porque construir aplicaciones destinadas a escalar de esta forma con el tiempo presenta algunos problemas, haciéndolas:
- Propenso a errores
-
addForm
del atributoonsubmit
podría ser fácilmente reescrito por otro JavaScript del lado del cliente en la página. Podríamos utilizaraddEventListener
en su lugar, pero esto plantea más preguntas:-
¿Dónde y cuándo lo limpiaríamos con
removeEventListener
? -
¿Acumularíamos muchos escuchadores de eventos con el tiempo si no tenemos cuidado con esto?
-
¿Qué sanciones pagaremos por ello?
-
¿Cómo encaja en esto la delegación de eventos?
-
- Impredecible
-
Nuestras fuentes de verdad están mezcladas: estamos guardando elementos de la lista en una matriz de JavaScript, pero dependemos de elementos existentes en el DOM (como un elemento con
id="list-parent"
) para completar nuestra aplicación. Debido a estas interdependencias entre JavaScript y HTML, tenemos que tener en cuenta algunas cosas más:-
¿Y si por error hay varios elementos con el mismo
id
? -
¿Y si el elemento no existe en absoluto?
-
¿Y si no es un
ul
? ¿Podemos anexar elementos de lista (li
elementos) a otros padres? -
¿Y si en lugar de eso utilizamos nombres de clases?
Nuestras fuentes de la verdad están mezcladas entre JavaScript y HTML, lo que hace que la verdad no sea fiable. Nos beneficiaría más tener una única fuente de verdad. Además, el JavaScript del lado del cliente añade y elimina elementos del DOM continuamente. Si confiamos en la existencia de estos elementos concretos, nuestra aplicación no tiene garantías de funcionar de forma fiable, ya que la interfaz de usuario no deja de actualizarse. En este caso, nuestra aplicación está llena de "efectos secundarios", en los que su éxito o fracaso depende de algún problema del usuario. React ha puesto remedio a esto defendiendo un modelo inspirado en la programación funcional, en el que los efectos secundarios se marcan y aíslan intencionadamente.
-
- Ineficiente
-
renderListItems
representa los elementos en la pantalla de forma secuencial. Cada mutación del DOM puede ser costosa desde el punto de vista computacional, sobre todo en lo que se refiere al cambio de disposición y a los reflujos. Como estamos en un planeta alienígena con una potencia de cálculo desconocida, esto puede ser bastante inseguro para el rendimiento en caso de listas grandes. Recuerda que pretendemos que nuestra aplicación web a gran escala sea utilizada por millones de personas en todo el mundo, incluidas aquellas con dispositivos de baja potencia de comunidades de todo el mundo sin acceso a los últimos y mejores procesadores Apple M3 Max. Lo que podría ser más ideal en este caso, en lugar de actualizar secuencialmente el DOM por cada elemento de la lista, sería realizar estas operaciones por lotes de alguna manera y aplicarlas todas al DOM al mismo tiempo. Pero quizás no merezca la pena que lo hagamos nosotros como ingenieros, porque quizás los navegadores acaben actualizando su forma de trabajar con actualizaciones rápidas del DOM y nos lo hagan automáticamente por lotes.
Estos son algunos de los problemas que han acosado a los desarrolladores web durante años, antes de que aparecieran React y otras abstracciones. Empaquetar el código de forma que fuera mantenible, reutilizable y predecible a escala era un problema sin mucho consenso estandarizado en la industria. Este dolor de crear interfaces de usuario fiables y escalables era compartido por muchas empresas web de la época. Fue en este punto de la web cuando vimos el surgimiento de múltiples soluciones basadas en JavaScript que pretendían resolver esto: Backbone, KnockoutJS, AngularJS y jQuery. Veamos estas soluciones por turnos y veamos cómo resolvían este problema. Esto nos ayudará a comprender en qué se diferencia React de estas soluciones, e incluso puede ser superior a ellas.
jQuery
Exploremos cómo resolvimos algunos de estos problemas anteriormente en la web utilizando herramientas anteriores a React y así aprenderemos por qué React es importante. Empezaremos con jQuery, y lo haremos revisitando nuestro ejemplo anterior del botón Me gusta.
Para recapitular, tenemos un botón "Me gusta" en el navegador que nos gustaría hacer interactivo:
<
button
id
=
"likeButton"
>
Like</
button
>
Con jQuery, le añadiríamos el comportamiento "me gusta" como hicimos antes, así
$
(
"#likeButton"
).
on
(
"click"
,
function
()
{
this
.
prop
(
"disabled"
,
true
);
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
this
.
text
()
===
"Like"
}),
})
.
then
(()
=>
{
this
.
text
(
this
.
text
()
===
"Like"
?
"Liked"
:
"Like"
);
})
.
catch
(()
=>
{
this
.
text
(
"Failed"
);
})
.
finally
(()
=>
{
this
.
prop
(
"disabled"
,
false
);
});
});
En este ejemplo, observamos que estamos vinculando datos a la interfaz de usuario y utilizando esta vinculación de datos para actualizar la interfaz de usuario in situ. jQuery, como herramienta, es bastante activo a la hora de manipular directamente la propia interfaz de usuario.
jQuery se ejecuta de forma muy "side-effectful", interactuando y alterando constantemente el estado fuera de su propio control. Decimos que tiene "efectos secundarios" porque permite modificaciones directas y globales de la estructura de la página desde cualquier parte del código, ¡incluso desde otros módulos importados o incluso desde la ejecución remota de scripts! Esto puede dar lugar a un comportamiento impredecible y a interacciones complejas difíciles de seguir y razonar, ya que los cambios en una parte de la página pueden afectar a otras partes de forma imprevista. Esta manipulación dispersa y desestructurada hace que el código sea difícil de mantener y depurar.
Los frameworks modernos abordan estos problemas proporcionando formas estructuradas y predecibles de actualizar la interfaz de usuario sin manipular directamente el DOM. Este patrón era habitual en la época, y es difícil de razonar y probar porque el mundo que rodea al código, es decir, el estado de la aplicación adyacente al código, cambia constantemente. En algún momento, tendríamos que detenernos y preguntarnos "¿cuál es el estado de la aplicación en el navegador en este momento?", una pregunta cada vez más difícil de responder a medida que crecía la complejidad de nuestras aplicaciones.
Además, este botón con jQuery es difícil de probar porque es sólo un manejador de eventos. Si tuviéramos que escribir una prueba, sería así:
test
(
"LikeButton"
,
()
=>
{
const
$button
=
$
(
"#likeButton"
);
expect
(
$button
.
text
()).
toBe
(
"Like"
);
$button
.
trigger
(
"click"
);
expect
(
$button
.
text
()).
toBe
(
"Liked"
);
});
El único problema es que $('#likeButton')
devuelve null
en el entorno de pruebas porque no es un navegador real. Tendríamos que simular el entorno del navegador para probar este código, lo que supone mucho trabajo. Éste es un problema común con jQuery: es difícil de probar porque es difícil aislar el comportamiento que añade. jQuery también depende en gran medida del entorno del navegador. Además, jQuery comparte la propiedad de la interfaz de usuario con el navegador , lo que dificulta el razonamiento y las pruebas: el navegador es el propietario de la interfaz, y jQuery es sólo un invitado. Esta desviación del paradigma del "flujo de datos unidireccional" era un problema habitual en las bibliotecas de la época.
Con el tiempo, jQuery empezó a perder popularidad a medida que la web evolucionaba y se hacía evidente la necesidad de soluciones más robustas y escalables. Aunque jQuery se sigue utilizando en muchas aplicaciones de producción, ya no es la solución a la que recurrir para crear aplicaciones web modernas. Estas son algunas de las razones por las que jQuery ha caído en desgracia:
- Peso y tiempos de carga
-
Una de las principales críticas a jQuery es su tamaño. Integrar toda la biblioteca jQuery en los proyectos web añade un peso extra, que puede ser especialmente gravoso para los sitios web que pretenden tiempos de carga rápidos. En la era actual de la navegación móvil, en la que muchos usuarios pueden tener conexiones de datos lentas o limitadas, cada kilobyte cuenta. Por tanto, la inclusión de toda la biblioteca jQuery puede afectar negativamente al rendimiento y la experiencia de los usuarios móviles.
Una práctica habitual antes de React era ofrecer configuradores para bibliotecas como jQuery y Mootools, donde los usuarios podían elegir la funcionalidad que deseaban. Aunque esto ayudaba a enviar menos código, introducía más complejidad en las decisiones que tenían que tomar los desarrolladores, y en elflujo de trabajo general de desarrollo.
- Redundancia con navegadores modernos
-
Cuando surgió jQuery, abordó muchas incoherencias entre navegadores y proporcionó a los desarrolladores una forma unificada de manejar estas diferencias en el contexto de la selección y posterior modificación de elementos en el navegador. A medida que la web evolucionaba, también lo hacían los navegadores. Muchas de las funciones que hicieron que jQuery fuera imprescindible, como la manipulación coherente del DOM o la funcionalidad orientada a la red en torno a la obtención de datos, ahora se admiten de forma nativa y coherente en los navegadores modernos. Utilizar jQuery para estas tareas en el desarrollo web contemporáneo puede considerarse redundante, ya que añade una capa innecesaria de complejidad.
document.querySelector
por ejemplo, sustituye fácilmente a la API del selector$
integrada en jQuery. - Consideraciones sobre el rendimiento
-
Aunque jQuery simplifica muchas tareas, a menudo lo hace a costa del rendimiento. Los métodos nativos de JavaScript en tiempo de ejecución mejoran con cada iteración del navegador y, por tanto, en algún momento pueden ejecutarse más rápido que sus equivalentes de jQuery. En proyectos pequeños, esta diferencia puede ser insignificante. Sin embargo, en aplicaciones web más grandes y complejas, estas complejidades pueden acumularse, dando lugar a un jank notable o a una capacidad de respuesta reducida.
Por estas razones, aunque jQuery desempeñó un papel fundamental en la evolución de la web y simplificó muchos retos a los que se enfrentaban los desarrolladores, el panorama web moderno ofrece soluciones nativas que a menudo hacen que jQuery sea menos relevante. Como desarrolladores, debemos sopesar la conveniencia de jQuery frente a sus posibles inconvenientes, especialmente en el contexto de los proyectos web actuales.
jQuery, a pesar de sus inconvenientes, supuso una revolución absoluta en la forma de interactuar con el DOM en su momento. Tanto es así que surgieron otras bibliotecas que utilizaban jQuery pero añadían más previsibilidad y reutilización a la mezcla. Una de esas bibliotecas fue Backbone, que fue un intento de resolver los mismos problemas que resuelve hoy React, pero mucho antes. Sumerjámonos en el tema.
Red troncal
Backbone, desarrollada a principios de la década de 2010, fue una de las primeras soluciones a los problemas que hemos estado explorando en el mundo anterior a React: disonancia de estado entre el navegador y JavaScript, reutilización de código, testabilidad y mucho más. Era una solución elegantemente sencilla: una biblioteca que proporcionaba una forma de crear "modelos" y "vistas". Backbone tenía su propia visión del patrón MVC (Modelo-Vista-Controlador) tradicional (ver Figura 1-1). Comprendamos un poco este patrón para ayudarnos a entender React y sentar las bases de undebate de mayor calidad.
El patrón MVC
El patrón MVC es una filosofía de diseño que divide las aplicaciones de software en tres componentes interconectados para separar las representaciones internas de la información del modo en que esa información se presenta al usuario o se acepta de él. Aquí tienes un desglose:
- Modelo
-
El Modelo es responsable de los datos y las reglas de negocio de la aplicación. El Modelo no conoce la Vista ni el Controlador, lo que garantiza que la lógica empresarial esté aislada de la interfaz de usuario.
- Ver
-
La Vista representa la interfaz de usuario de la aplicación. Muestra datos del Modelo al usuario y envía órdenes de usuario al Controlador. La Vista es pasiva, es decir, espera a que el Modelo proporcione datos para mostrarlos y no obtiene ni guarda datos directamente. La Vista tampoco gestiona por sí misma la interacción con el usuario, sino que delega esta responsabilidad en el siguiente componente: el Controlador.
- Controlador
-
El Controlador actúa como interfaz entre el Modelo y la Vista. Toma la entrada del usuario de la Vista, la procesa (con posibles actualizaciones del Modelo) y devuelve la pantalla de salida a la Vista. El Controlador desacopla el Modelo de la Vista, haciendo más flexible la arquitectura del sistema.
La principal ventaja del patrón MVC es la separación de preocupaciones, lo que significa que la lógica empresarial, la interfaz de usuario y la entrada del usuario están separadas en diferentes secciones de la base de código. Esto no sólo hace que la aplicación sea más modular, sino también más fácil de mantener, escalar y probar. El patrón MVC se utiliza ampliamente en aplicaciones web, y muchos marcos como Django, Ruby on Rails y ASP.NET MVC ofrecen soporte integrado para él.
El patrón MVC ha sido un elemento básico en el diseño de software durante muchos años, especialmente en el desarrollo web. Sin embargo, a medida que las aplicaciones web han evolucionado y han aumentado las expectativas de los usuarios de interfaces interactivas y dinámicas, se han puesto de manifiesto algunas limitaciones del MVC tradicional. A continuación te explicamos en qué puede fallar el MVC y cómo resuelve React estos problemas:
- Interactividad compleja y gestión de estados
-
Las arquitecturas MVC tradicionales suelen tener problemas cuando se trata de gestionar interfaces de usuario complejas con muchos elementos interactivos. A medida que crece una aplicación, la gestión de los cambios de estado y sus efectos en las distintas partes de la interfaz de usuario puede resultar engorrosa, ya que los controladores se amontonan y, a veces, pueden entrar en conflicto con otros controladores, ya que algunos controladores controlan vistas que no les representan, o la separación entre componentes MVC no está bien delimitada en el código del producto.
React, con su arquitectura basada en componentes y DOM virtual, facilita el razonamiento sobre los cambios de estado y sus efectos en la interfaz de usuario al plantear esencialmente que los componentes de la interfaz de usuario son como una función: reciben entradas (props) y devuelven salidas basadas en esas entradas (elementos). Este modelo mental simplificó radicalmente el patrón MVC porque las funciones son bastante ubicuas en JavaScript y mucho más accesibles en comparación con un modelo mental externo que no es nativo del lenguaje de programación como MVC.
- Vinculación bidireccional de datos
-
Algunos marcos MVC utilizan la vinculación bidireccional de datos, lo que puede provocar efectos secundarios no deseados si no se gestiona con cuidado, ya que en algunos casos la vista se desincroniza con el modelo o viceversa. Además, con la vinculación bidireccional de datos, la cuestión de la propiedad de los datos suele tener una respuesta burda, con una separación poco clara de las preocupaciones. Esto es especialmente interesante porque, aunque MVC es un modelo probado para los equipos que comprenden plenamente la forma adecuadade separar las preocupaciones para sus casos de uso, estas reglas de separación rara vez seaplican -especialmente cuando se enfrentan a una salida de alta velocidad y a un rápidocrecimiento de lastartup- y, por lo tanto, la separación de preocupaciones, uno de los mayores puntos fuertes de MVC, a menudo se convierte en una debilidad por esta falta de aplicación.
React aprovecha un patrón contrario a la vinculación de datos bidireccional llamado "flujo de datos unidireccional" (más sobre esto más adelante) para priorizar e incluso imponer un flujo de datos unidireccional a través de sistemas como Forget (del que también hablaremos más adelante en el libro). Estos enfoques hacen que las actualizaciones de la interfaz de usuario sean más predecibles, nos permiten separar las preocupaciones con mayor claridad y, en última instancia, son propicios para los equipos de software de hipercrecimiento de alta velocidad.
- Acoplamiento apretado
-
En algunas implementaciones MVC, el Modelo, la Vista y el Controlador pueden llegar a estar estrechamente acoplados, lo que dificulta cambiar o refactorizar uno sin afectar a los demás. React fomenta un enfoque más modular y desacoplado con su modelo basado en componentes, permitiendo y apoyando la colocación de dependencias cerca de sus representaciones de interfaz de usuario.
No necesitamos entrar demasiado en los detalles de este patrón, ya que éste es un libro de React, pero para nuestros propósitos aquí, los modelos eran conceptualmente fuentes de datos, y las vistas eran conceptualmente interfaces de usuario que consumían y renderizaban esos datos. Backbone exportaba cómodas API para trabajar con estos modelos y vistas, y proporcionaba una forma de conectar los modelos y las vistas entre sí. Esta solución era muy potente y flexible para su época. También era una solución escalable y permitía a los desarrolladores probar su código de forma aislada.
Como ejemplo, aquí está nuestro anterior ejemplo de botón, esta vez utilizandoBackbone:
const
LikeButton
=
Backbone
.
View
.
extend
({
tagName
:
"button"
,
attributes
:
{
type
:
"button"
,
},
events
:
{
click
:
"onClick"
,
},
initialize
()
{
this
.
model
.
on
(
"change"
,
this
.
render
,
this
);
},
render
()
{
this
.
$el
.
text
(
this
.
model
.
get
(
"liked"
)
?
"Liked"
:
"Like"
);
return
this
;
},
onClick
()
{
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
this
.
model
.
get
(
"liked"
)
}),
})
.
then
(()
=>
{
this
.
model
.
set
(
"liked"
,
!
this
.
model
.
get
(
"liked"
));
})
.
catch
(()
=>
{
this
.
model
.
set
(
"failed"
,
true
);
})
.
finally
(()
=>
{
this
.
model
.
set
(
"pending"
,
false
);
});
},
});
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
}),
});
document
.
body
.
appendChild
(
likeButton
.
render
().
el
);
¿Notas cómo LikeButton
extiende a Backbone.View
y cómo tiene un métodorender
que devuelve this
? Seguiremos viendo un métodorender
similar en React, pero no nos adelantemos. También cabe señalar aquí que Backbone no incluía una implementación real para render
. En su lugar, o bien mutabas manualmente el DOM mediante jQuery, o bien utilizabas un sistema de plantillas como Handlebars.
Backbone expuso una API encadenable que permitía a los desarrolladores colocar la lógica como propiedades en los objetos. Comparando esto con nuestro ejemplo anterior, podemos ver que Backbone ha hecho mucho más cómodo crear un botón que sea interactivo y actualice la interfaz de usuario en respuesta a eventos.
También lo hace de una forma más estructurada agrupando la lógica. Ten en cuenta también que Backbone ha hecho que sea más accesible probar este botón de forma aislada porque podemos crear una instancia de LikeButton
y luego llamar a su método render
para probarlo.
Probamos este componente así:
test
(
"LikeButton initial state"
,
()
=>
{
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
// Initial state set to not liked
}),
});
likeButton
.
render
();
// Ensure render is called to reflect the initial state
// Check the text content to be "Like" reflecting the initial state
expect
(
likeButton
.
el
.
textContent
).
toBe
(
"Like"
);
});
Incluso podemos probar el comportamiento del botón después de que cambie su estado, como en el caso de un evento de clic, así:
test
(
"LikeButton"
,
async
()
=>
{
// Mark the function as async to handle promise
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
}),
});
expect
(
likeButton
.
render
().
el
.
textContent
).
toBe
(
"Like"
);
// Mock fetch to prevent actual HTTP request
global
.
fetch
=
jest
.
fn
(()
=>
Promise
.
resolve
({
json
:
()
=>
Promise
.
resolve
({
liked
:
true
}),
})
);
// Await the onClick method to ensure async operations are complete
await
likeButton
.
onClick
();
expect
(
likeButton
.
render
().
el
.
textContent
).
toBe
(
"Liked"
);
// Optionally, restore fetch to its original implementation if needed
global
.
fetch
.
mockRestore
();
});
Por esta razón, Backbone era una solución muy popular en aquel momento. La alternativa era escribir un montón de código difícil de probar y difícil de razonar, sin garantías de que el código funcionara como se esperaba de forma fiable. Por tanto, Backbone fue una solución muy bienvenida. Aunque ganó popularidad en sus inicios por su sencillez y flexibilidad, no está exento de críticas. He aquí algunos de los aspectos negativos asociados a Backbone.js:
- Código verborreico y repetitivo
-
Una de las críticas frecuentes a Backbone.js es la cantidad de código repetitivo que los desarrolladores tienen que escribir. Para aplicaciones sencillas, esto puede no ser un gran problema, pero a medida que la aplicación crece, también lo hace el código repetitivo, dando lugar a código potencialmente redundante y difícil de mantener.
- Falta de vinculación bidireccional de datos
-
A diferencia de algunos de sus contemporáneos, Backbone.js no ofrece enlace de datos bidireccional incorporado. Esto significa que si los datos cambian, el DOM no se actualiza automáticamente, y viceversa. Los desarrolladores a menudo tienen que escribir código personalizado o utilizar plug-ins para conseguir esta funcionalidad.
- Arquitectura basada en eventos
-
Las actualizaciones de los datos del modelo pueden desencadenar numerosos eventos en toda la aplicación. Esta cascada de eventos puede llegar a ser inmanejable, provocando una situación en la que no está claro cómo el cambio de un único dato afectará al resto de la aplicación, lo que dificulta la depuración y el mantenimiento. Para resolver estos problemas,los desarrolladores a menudo necesitaban utilizar prácticas cuidadosas de gestión de eventos para evitar el efecto dominó de las actualizaciones en toda la aplicación.
- Falta de componibilidad
-
Backbone.js carece de funciones integradas para anidar fácilmente vistas, lo que puede dificultar la composición de interfaces de usuario complejas. React, en cambio, permite anidar componentes sin fisuras a través de la proposición children, lo que simplifica mucho la construcción de intrincadas jerarquías de interfaz de usuario. Marionette.js, una extensión de Backbone, intentó resolver algunos de estos problemas de composición, pero no proporciona una solución tan integrada como el modelo de componentes de React.
Aunque Backbone.js tiene su conjunto de retos, es esencial recordar que ninguna herramienta o marco de trabajo es perfecto. La mejor elección depende a menudo de las necesidades específicas del proyecto y de las preferencias del equipo de desarrollo. También merece la pena señalar lo mucho que dependen las herramientas de desarrollo web de una comunidad fuerte para prosperar, y por desgracia Backbone.js ha visto disminuir su popularidad en los últimos años, especialmente con la llegada de React. Algunos dirían que React lo mató, pero nos reservaremos el juicio por ahora.
KnockoutJS
Comparemos este enfoque con otra solución popular en aquel momento: KnockoutJS. KnockoutJS, desarrollada a principios de la década de 2010, era una biblioteca que proporcionaba una forma de crear "observables" y "bindings", haciendo uso del seguimiento de dependenciascada vez que cambiaba el estado.
KnockoutJS fue una de las primeras, si no la primera, biblioteca JavaScript reactiva, donde la reactividad se define como valores que se actualizan en respuesta a cambios de estado de forma observable. Las versiones modernas de este estilo de reactividad a veces se denominan "señales" en y prevalecen en bibliotecas como Vue.js, SolidJS, Svelte, Qwik, Angular moderno, etc. En el Capítulo 10 las veremos con más detalle.
Los observables eran conceptualmente fuentes de datos, y los enlaces eran conceptualmente interfaces de usuario que consumían y representaban esos datos: los observables eran como modelos, y los enlaces eran como vistas.
Sin embargo, como una pequeña evolución del patrón MVC del que hablamos anteriormente, KnockoutJS funcionaba más bien según un patrón de tipo Modelo-Vista-Vista-Modelo o MVVM (ver Figura 1-2). Vamos a entender este patrón con cierto detalle.
Patrón MVVM
El patrón MVVM es un patrón de diseño arquitectónico especialmente popular en aplicaciones con interfaces de usuario ricas, como las creadas con plataformas como WPF y Xamarin. MVVM es una evolución del patrón tradicional Modelo-Vista-Controlador (MVC), adaptado a las plataformas modernas de desarrollo de interfaces de usuario en las que la vinculación de datoses una característica destacada. Aquí tienes un desglose de los componentes MVVM:
- Modelo
- Ver
-
-
Muestra información al usuario y recibe sus aportaciones.
-
En MVVM, la Vista es pasiva y no contiene ninguna lógica de aplicación. En su lugar, se vincula de forma declarativa al ViewModel, reflejando los cambios automáticamente mediante mecanismos de vinculación de datos.
- ViewModel
-
-
Expone datos y comandos para que la Vista se vincule a ellos. Los datos suelen estar en un formato listo para su visualización.
-
Maneja las entradas del usuario, a menudo mediante patrones de comandos.
-
Contiene la lógica de presentación y transforma los datos del Modelo en un formato que la Vista pueda mostrar fácilmente.
-
En particular, el ModeloVista no conoce la Vista concreta que lo utiliza, lo que permite una arquitectura desacoplada.
La ventaja clave del patrón MVVM es la separación de preocupacionessimilar a MVC, que conduce a:
- Comprobabilidad
-
El desacoplamiento de ViewModel y View facilita la escritura de pruebas unitarias para la lógica de presentación sin implicar a la interfaz de usuario.
- Reutilización
-
El ViewModel puede reutilizarse en diferentes vistas o plataformas.
- Mantenibilidad
-
Con una separación clara, es más fácil gestionar, ampliar y refactorizar el código.
- Vinculación de datos
-
El patrón destaca en plataformas que admiten la vinculación de datos, reduciendo la cantidad de código repetitivo necesario para actualizar la interfaz de usuario.
Ya que hemos hablado de los patrones MVC y MVVM, vamos a contrastarlos rápidamente para que podamos entender las diferencias entre ellos (ver Tabla 1-1).
Criterios | MVC | MVVM |
---|---|---|
Objetivo principal |
Principalmente para aplicaciones web, separando la interfaz de usuario de la lógica. |
Diseñado para aplicaciones de interfaz de usuario enriquecidas, especialmente con enlace de datos bidireccional, como las de escritorio o SPA. |
Componentes |
Modelo: datos y lógica empresarial. Vista: interfaz de usuario. Controlador: gestiona la entrada del usuario, actualiza la Vista. |
Modelo: datos y lógica empresarial. Vista: elementos de la interfaz de usuario. ViewModel: puente entre el Modelo y la Vista. |
Flujo de datos |
La entrada del usuario la gestiona el Controlador, que actualiza el Modelo y luego la Vista. |
La Vista se vincula directamente a la VistaModelo. Los cambios en la Vista se reflejan automáticamente en la VistaModelo y viceversa. |
Desacoplamiento |
La Vista suele estar estrechamente acoplada al Controlador. |
Alto desacoplamiento, ya que ViewModel no conoce la Vista concreta que lo utiliza. |
Interacción con el usuario |
Gestionado por el Interventor. |
Se gestiona mediante enlaces de datos y comandos en el ViewModel. |
Adecuación de la plataforma |
Común en el desarrollo de aplicaciones web (por ejemplo, Ruby on Rails, Django, ASP.NET MVC). |
Adecuado para plataformas que admitan una sólida vinculación de datos (por ejemplo, WPF, Xamarin). |
A partir de esta breve comparación, podemos ver que la diferencia real entre los patrones MVC y MVVM es de acoplamiento y vinculación: sin un Controlador entre un Modelo y una Vista, la propiedad de los datos es más clara y está más cerca del usuario. React mejora aún más a MVVM con su flujo de datos unidireccional, del que hablaremos dentro de un rato, estrechando aún más la propiedad de los datos, de forma que el estado es propiedad de componentes específicos que los necesitan. Por ahora, volvamos a KnockoutJS y a su relación con React.
KnockoutJS exporta APIs para trabajar con estos observables y bindings. Veamos cómo implementaríamos el botón Me gusta en KnockoutJS. Esto nos ayudará a entender "por qué React" un poco mejor. Aquí está la versión KnockoutJS de nuestro botón:
function
createViewModel
({
liked
})
{
const
isPending
=
ko
.
observable
(
false
);
const
hasFailed
=
ko
.
observable
(
false
);
const
onClick
=
()
=>
{
isPending
(
true
);
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
()
}),
})
.
then
(()
=>
{
liked
(
!
liked
());
})
.
catch
(()
=>
{
hasFailed
(
true
);
})
.
finally
(()
=>
{
isPending
(
false
);
});
};
return
{
isPending
,
hasFailed
,
onClick
,
liked
,
};
}
ko
.
applyBindings
(
createViewModel
({
liked
:
ko
.
observable
(
false
)
}));
En KnockoutJS, un "modelo de vista" es un objeto JavaScript que contiene claves y valores que vinculamos a varios elementos de nuestra página mediante el atributodata-bind
. En KnockoutJS no hay "componentes" ni "plantillas", sólo un modelo de vista y una forma de vincularlo a un elemento del navegador.
Nuestra función createViewModel
es como crearíamos un modelo de vista con Knockout. A continuación, utilizamos ko.applyBindings
para conectar el modelo de vista al entorno anfitrión (el navegador). La función ko.applyBindings
toma un modelo de vista y encuentra todos los elementos del navegador que tienen un atributo data-bind
, que Knockout utiliza para vincularlos al modelo de vista.
Un botón en nuestro navegador estaría vinculado a las propiedades de este modelo de vista como :
<
button
data-bind
=
"click:
onClick
,
text:
liked
?
'
Liked
'
:
isPending
?
[...]
></
button
>
Ten en cuenta que este código se ha truncado para simplificar.
Vinculamos el elemento HTML al "modelo de vista" que hemos creado utilizando nuestra funcióncreateViewModel
, y el sitio se vuelve interactivo. Como puedes imaginar, suscribirse explícitamente a los cambios en los observables y luego actualizar la interfaz de usuario en respuesta a estos cambios es mucho trabajo. KnockoutJS era una gran biblioteca para su época, pero también requería mucho código repetitivo para hacer las cosas.
Además, los modelos de vista a menudo se hacían muy grandes y complejos, lo que provocaba una incertidumbre cada vez mayor en torno a las refactorizaciones y optimizaciones del código. Al final, acabábamos teniendo modelos de vista monolíticos y verbosos que resultaban difíciles de probar y razonar. Aun así, KnockoutJS era una solución muy popular y una gran biblioteca para su época. También era relativamente fácil de probar de forma aislada, lo que era una gran ventaja.
Para la posteridad, así es como probaríamos este botón en KnockoutJS:
test
(
"LikeButton"
,
()
=>
{
const
viewModel
=
createViewModel
({
liked
:
ko
.
observable
(
false
)
});
expect
(
viewModel
.
liked
()).
toBe
(
false
);
viewModel
.
onClick
();
expect
(
viewModel
.
liked
()).
toBe
(
true
);
});
AngularJS
AngularJS fue desarrollado por Google en 2010. Fue un marco JavaScript pionero que tuvo un impacto significativo en el panorama del desarrollo web. Contrastaba fuertemente con las bibliotecas y marcos que hemos estado debatiendo al incorporar varias características innovadoras, cuyas ondas pueden verse en bibliotecas posteriores, incluida React. Mediante una comparación detallada de AngularJS con estas otras bibliotecas y un vistazo a sus características fundamentales, vamos a intentar comprender el camino que labró para React.
Vinculación bidireccional de datos
Vinculación bidireccional de datos era una característica distintiva de AngularJS que simplificaba enormemente la interacción entre la interfaz de usuario y los datos subyacentes. Si el modelo (los datos subyacentes) cambia, la vista (la interfaz de usuario) se actualiza automáticamente para reflejar el cambio, y viceversa. Esto contrastaba fuertemente con librerías como jQuery, en las que los desarrolladores tenían que manipular manualmente el DOM para reflejar cualquier cambio en los datos y capturar las entradas del usuario para actualizar los datos.
Consideremos una sencilla aplicación AngularJS en la que la vinculación bidireccional de datos desempeña un papel crucial:
<!DOCTYPE html>
<
html
>
<
head
>
<
script
src
=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"
>
</
script
>
</
head
>
<
body
ng-app
=
""
>
<
p
>
Name:<
input
type
=
"text"
ng-model
=
"name"
/></
p
>
<
p
ng-if
=
"name"
>
Hello, {{name}}!</
p
>
</
body
>
</
html
>
En esta aplicación, la directiva ng-model
vincula el valor del campo de entrada a la variable name
. A medida que escribes en el campo de entrada, el modelo name
se actualiza, y la vista -en este caso, el saludo"Hello, {{name}}!"
- se actualiza en tiempo real.
Arquitectura modular
AngularJS introdujo una arquitectura modular que permitía a los desarrolladores separar lógicamente los componentes de su aplicación. Cada módulo podía encapsular una funcionalidad y podía desarrollarse, probarse y mantenerse de forma independiente. Algunos llamarían a esto un precursor del modelo de componentes de React, pero esto es discutible.
He aquí un ejemplo rápido:
var
app
=
angular
.
module
(
"myApp"
,
[
"ngRoute"
,
"appRoutes"
,
"userCtrl"
,
"userService"
,
]);
var
userCtrl
=
angular
.
module
(
"userCtrl"
,
[]);
userCtrl
.
controller
(
"UserController"
,
function
(
$scope
)
{
$scope
.
message
=
"Hello from UserController"
;
});
var
userService
=
angular
.
module
(
"userService"
,
[]);
userService
.
factory
(
"User"
,
function
(
$http
)
{
//...
});
En el ejemplo anterior, el módulo myApp
depende de varios otros módulos: ngRoute
, appRoutes
, userCtrl
, y userService
. Cada módulo dependiente podía estar en su propio archivo JavaScript, y podía desarrollarse por separado del módulo principal myApp
. Este concepto era significativamente diferente del de jQuery y Backbone.js, que no tenían un concepto de "módulo" en este sentido.
Inyectamos estas dependencias (appRoutes
, userCtrl
, etc.) en nuestra raíz app
utilizando un patrón llamado inyección de dependencias que se popularizó en Angular. Ni que decir tiene que este patrón prevalecía antes de que se estandarizaran los módulos de JavaScript. Desde entonces, las declaraciones import
yexport
se impusieron rápidamente. Para contrastar estas dependencias con los componentes React, hablemos un poco más de la inyección de dependencias.
Inyección de dependencia
La inyección de dependencias (DI) es un patrón de diseño en el que un objeto recibe sus dependencias en lugar de crearlas. AngularJS incorporó este patrón de diseño en su núcleo, lo que no era una característica común en otras bibliotecas JavaScript en aquel momento. Esto tuvo un profundo impacto en la forma de crear y gestionar módulos y componentes, promoviendo un mayor grado de modularidad y reutilización.
Aquí tienes un ejemplo de cómo funciona DI en AngularJS:
var
app
=
angular
.
module
(
"myApp"
,
[]);
app
.
controller
(
"myController"
,
function
(
$scope
,
myService
)
{
$scope
.
greeting
=
myService
.
sayHello
();
});
app
.
factory
(
"myService"
,
function
()
{
return
{
sayHello
:
function
()
{
return
"Hello, World!"
;
},
};
});
En el ejemplo, myService
es un servicio que se inyecta en el controladormyController
a través de DI. El controlador no necesita saber cómo crear el servicio. Simplemente declara el servicio como una dependencia, y AngularJS se encarga de crearlo e inyectarlo. Esto simplifica la gestión de dependencias y mejora la comprobabilidad y reutilización de los componentes.
Comparación con Backbone.js y Knockout.js
Backbone.js y Knockout.js eran dos bibliotecas populares que se utilizaban en la época en que se introdujo AngularJS. Ambas bibliotecas tenían sus puntos fuertes, pero carecían de algunas características que se incorporaron a AngularJS.
Backbone.js, por ejemplo, daba a los desarrolladores más control sobre su código y era menos opinable que AngularJS. Esta flexibilidad era a la vez un punto fuerte y un punto débil: permitía más personalización, pero también requería más código repetitivo. AngularJS, con su enlace de datos bidireccional y DI, permitía más estructura. Tenía más opiniones que conducían a una mayor velocidad del desarrollador: algo que vemos con frameworks modernos como Next.js, Remix, etc. Esta es una de las formas en que AngularJS se adelantó mucho a su tiempo.
Backbone tampoco tenía una respuesta para mutar directamente la vista (el DOM) y a menudo dejaba esto en manos de los desarrolladores. AngularJS se ocupaba de las mutaciones del DOM con su enlace de datos bidireccional, lo que era una gran ventaja.
Knockout.js se centraba principalmente en la vinculación de datos y carecía de algunas de las otras potentes herramientas que ofrecía AngularJS, como DI y una arquitectura modular. AngularJS, al ser un framework en toda regla, ofrecía una solución más completa para crear aplicaciones de una sola página (SPA). Aunque AngularJS dejó de utilizarse, hoy en día su variante más reciente, llamada Angular, ofrece las mismas ventajas integrales, aunque mejoradas, que lo convierten en una opción ideal para aplicaciones a gran escala.
Ventajas y desventajas de AngularJS
AngularJS (1.x) representó un salto significativo en las prácticas de desarrollo web cuando se introdujo. Sin embargo, a medida que el panorama del desarrollo web seguía evolucionando rápidamente, ciertos aspectos de AngularJS se consideraron limitaciones o puntos débiles que contribuyeron a su relativo declive. Algunos de ellos son
- Rendimiento
-
AngularJS tenía problemas de rendimiento, sobre todo en aplicaciones a gran escala con enlaces de datos complejos. El ciclo de resumen de AngularJS, una función esencial para la detección de cambios, podía provocar actualizaciones lentas e interfaces de usuario lentas en aplicaciones de gran tamaño. El enlace de datos bidireccional, aunque innovador y útil en muchas situaciones, también contribuía a los problemas de rendimiento.
- Complejidad
-
AngularJS introdujo una serie de conceptos novedosos, como directivas, controladores, servicios, inyección de dependencias, fábricas y mucho más. Aunque estas características hicieron que AngularJS fuera potente, también lo hicieron complejo y difícil de aprender, especialmente para los principiantes. Un debate habitual, por ejemplo, era "¿esto debería ser una fábrica o un servicio?", lo que dejaba perplejos a varios equipos de desarrolladores.
- Problemas de migración a Angular 2+
-
Cuando se anunció Angular 2, no era compatible con AngularJS 1.x. y requería que el código se escribiera en Dart y/o TypeScript. Esto significaba que los desarrolladores tenían que reescribir partes significativas de su código para actualizar a Angular 2, lo que se consideraba un gran obstáculo. La introducción de Angular 2+ esencialmente dividió a la comunidad Angular y causó confusión, allanando el camino para React.
- Sintaxis compleja en las plantillas
-
El hecho de que AngularJS permitiera expresiones JavaScript complejas dentro de los atributos de las plantillas, como
on-click="$ctrl.some.deeply.nested.field = 123"
, resultaba problemático porque daba lugar a una mezcla de presentación y lógica empresarial dentro del marcado. Este enfoque creaba problemas de mantenimiento, ya que descifrar y gestionar el código entrelazado resultaba engorroso.Además, depurar era más difícil porque las capas de plantillas no estaban diseñadas intrínsecamente para manejar lógica compleja, y cualquier error que surgiera de estas expresiones en línea podía ser difícil de localizar y resolver. Además, estas prácticas violaban el principio de separación de preocupaciones, que es una filosofía de diseño fundamental que aboga por el manejo diferenciado de los distintos aspectos de una aplicación para mejorar la calidad y la mantenibilidad del código.
En teoría, una plantilla debería llamar a un método del controlador para realizar una actualización, pero nada lo restringe.
- Ausencia de seguridad de tipo
-
Las plantillas en AngularJS no funcionaban con comprobadores de tipos estáticos como TypeScript, lo que dificultaba la detección de errores en las primeras fases del proceso de desarrollo. Esto era un inconveniente importante, especialmente para aplicaciones a gran escala en las que la seguridad de tipos es crucial para la mantenibilidad y la escalabilidad.
- Modelo confuso
$scope
-
El objeto
$scope
en AngularJS era a menudo fuente de confusión debido a su papel en la vinculación de datos y a su comportamiento en diferentes contextos, ya que servía de pegamento entre la vista y el controlador, pero su comportamiento no siempre era intuitivo ni predecible.Esto provocaba complejidades, especialmente para los recién llegados, a la hora de comprender cómo se sincronizaban los datos entre el modelo y la vista. Además,
$scope
podía heredar propiedades de ámbitos padre en controladores anidados, lo que dificultaba el seguimiento de dónde se había definido o modificado originalmente una propiedad concreta de$scope
.Esta herencia podía causar efectos secundarios inesperados en la aplicación, sobre todo cuando se trataba de ámbitos anidados en los que los ámbitos padre e hijo podían afectarse mutuamente de forma inadvertida. El concepto de jerarquía de ámbitos y la herencia prototípica en la que se basaba a menudo estaban reñidos con las reglas de ámbito léxico más tradicionales y familiares de JavaScript, lo que añadía otra capa de complejidad de aprendizaje.
React, por ejemplo, coloca el estado con el componente que lo necesita, y así evita este problema por completo.
- Herramientas de desarrollo limitadas
-
AngularJS no ofrecía amplias herramientas para desarrolladores para la depuración y la creación de perfiles de rendimiento, especialmente en comparación con las DevTools disponibles en React como Replay.io, que permite amplias capacidades en torno a la depuración de viajes en el tiempo para aplicaciones React.
Entra en React
Fue por entonces cuando React saltó a la fama. Una de las ideas centrales que presentaba React era la arquitectura basada en componentes. Aunque la implementación es diferente, la idea subyacente es similar: es óptimo construir interfaces de usuario para la web y otras plataformas componiendo componentes reutilizables.
Mientras AngularJS utilizaba directivas para vincular vistas a modelos, React introdujo JSX y un modelo de componentes radicalmente más sencillo. Sin embargo, sin la base sentada por AngularJS al promover una arquitectura basada en componentes a través de los módulos Angular, algunos argumentarían que la transición al modelo de React podría no haber sido tan fluida.
En AngularJS, el modelo de enlace de datos bidireccional era el estándar del sector; sin embargo, también tenía algunas desventajas, como los posibles problemas de rendimiento en aplicaciones grandes. React aprendió de esto e introdujo un patrón de flujo de datos unidireccional, dando a los desarrolladores más control sobre sus aplicaciones y facilitando la comprensión de cómo cambian los datos con el tiempo.
React también introdujo el DOM virtual, como leeremos en el Capítulo 3: un concepto que mejoraba el rendimiento al minimizar la manipulación directa del DOM. AngularJS, por otro lado, a menudo manipulaba directamente el DOM, lo que podía provocar problemas de rendimiento y otros problemas de estado incoherente que comentamos recientemente con jQuery.
Dicho esto, AngularJS representó un cambio significativo en las prácticas de desarrollo web, y seríamos negligentes si no mencionáramos que AngularJS no sólo revolucionó el panorama del desarrollo web cuando se introdujo, sino que también allanó el camino para la evolución de futuros frameworks y bibliotecas, siendo React uno de ellos.
Exploremos cómo encaja React en todo esto y de dónde vino React en este momento de la historia. En esta época, las actualizaciones de la interfaz de usuario eran todavía un problema relativamente difícil y sin resolver. Hoy en día están lejos de estar resueltos, pero React los ha hecho notablemente menos difíciles, y ha inspirado a otras bibliotecas como SolidJS, Qwik y otras. El Facebook de Meta no fue una excepción al problema de la complejidad y escala de la interfaz de usuario. Como resultado, Meta creó una serie de soluciones internas complementarias a lo que ya existía en ese momento. La primera de ellas fue BoltJS: una herramienta que los ingenieros de Facebook dirían que "atornillaba" un montón de cosas que les gustaban. Se ensambló una combinación de herramientas para hacer más intuitivas las actualizaciones de la interfaz de usuario web de Facebook.
Por esa época, el ingeniero de Facebook Jordan Walke tuvo una idea radical que acababa con el statu quo de la época y sustituía por completo partes mínimas de las páginas web por otras nuevas a medida que se producían actualizaciones. Como hemos visto anteriormente, las bibliotecas de JavaScript gestionarían las relaciones entre las vistas (interfaces de usuario) y los modelos (conceptualmente, fuentes de datos) mediante un paradigma denominado enlace de datos bidireccional. A la luz de las limitaciones de este modelo, como hemos visto antes, la idea de Jordan fue utilizar en su lugar un paradigma llamado flujo de datos unidireccional. Se trataba de un paradigma mucho más sencillo, y era mucho más fácil mantener sincronizadas las vistas y los modelos. Así nació la arquitectura unidireccional que acabaría siendo la base de React.
La propuesta de valor de React
Vale, se acabó la lección de historia. Esperemos que ahora tengamos suficiente contexto para empezar a entender por qué React existe. Dado lo fácil que era caer en el pozo del código JavaScript inseguro, impredecible e ineficiente a escala, necesitábamos una solución que nos condujera hacia un pozo de éxito en el que accidentalmente ganáramos. Hablemos de cómo lo hace exactamente React.
Código declarativo frente a imperativo
React proporciona una abstracción declarativa sobre el DOM. Hablaremos de cómo lo hace con más detalle más adelante en el libro, pero esencialmente nos proporciona una forma de escribir código que expresa lo que queremos ver, mientras que luego se ocupa de cómo sucede, asegurándose de que nuestra interfaz de usuario se crea y funciona de forma segura, predecible y eficiente.
Consideremos la aplicación de listas que hemos creado antes. En React, podríamos reescribirla así:
function
MyList
()
{
const
[
items
,
setItems
]
=
useState
([
"I love"
]);
return
(
<
div
>
<
ul
>
{
items
.
map
((
i
)
=>
(
<
li
key
=
{
i
/* keep items unique */
}
>
{
i
}
<
/li>
))}
<
/ul>
<
NewItemForm
onAddItem
=
{(
newItem
)
=>
setItems
([...
items
,
newItem
])}
/>
<
/div>
);
}
Fíjate cómo en return
, escribimos literalmente algo que parece HTML: se parece a lo que queremos ver. Quiero ver una caja con unNewItemForm
, y una lista. Bum. ¿Cómo llega ahí? Eso lo tiene que averiguar React. ¿Agregamos por lotes los elementos de la lista para añadir trozos de una vez? ¿Los añadimos secuencialmente, uno a uno? React se ocupa de cómo se hace esto, mientras que nosotros nos limitamos a describir lo que queremos que se haga. En capítulos posteriores, nos sumergiremos en React y exploraremos cómo lo hace exactamente en el momento de escribir esto.
¿Dependemos entonces de los nombres de clase para hacer referencia a los elementos HTML? ¿HacemosgetElementById
en JavaScript? No. React crea "elementos React" únicos para nosotros bajo el capó que utiliza para detectar cambios y realizar actualizaciones incrementales, de modo que no necesitamos leer nombres de clase y otros identificadores del código de usuario cuya existencia no podemos garantizar: nuestra fuente de verdad pasa a ser exclusivamente JavaScript con React.
Exportamos nuestro componente MyList
a React, y React lo pone en pantalla por nosotros de forma segura, predecible y eficaz, sin hacer preguntas. El trabajo del componente consiste simplemente en devolver una descripción del aspecto que debe tener esta parte de la interfaz de usuario. Para ello utiliza unDOM virtual (vDOM), que es una descripción ligera de la estructura prevista de la IU. React compara el DOM virtual después de que se produzca una actualización con el DOM virtual antes de que se produzca una actualización, y lo convierte en pequeñas actualizaciones de alto rendimiento del DOM real para hacerlo coincidir con el DOM virtual. Así es como React puede actualizar el DOM.
El DOM virtual
El DOM virtual es un concepto de programación que representa el DOM real pero como un objeto JavaScript. Si esto te resulta demasiado complicado por ahora, no te preocupes: el Capítulo 3 está dedicado a ello y lo explica con más detalle. Por ahora, sólo es importante saber que el DOM virtual permite a los desarrolladores actualizar la interfaz de usuario sin manipular directamente el DOM real. React utiliza el DOM virtual para realizar un seguimiento de los cambios en un componente y vuelve a renderizar el componente sólo cuando es necesario. Este enfoque es más rápido y eficiente que actualizar todo el árbol DOM cada vez que se produce un cambio.
En React, el DOM virtual es una representación ligera del árbol DOM real. Es un objeto JavaScript plano que describe la estructura y las propiedades de los elementos de la interfaz de usuario. React crea y actualiza el DOM virtual para que coincida con el árbol DOM real, y cualquier cambio realizado en el DOM virtual se aplica al DOM real mediante un proceso llamado reconciliación.
El Capítulo 4 está dedicado a esto, pero para nuestra discusión contextual aquí, veamos un pequeño resumen con algunos ejemplos. Para entender cómo funciona el DOM virtual, volvamos a nuestro ejemplo del botón Me gusta. Crearemos un componente React que muestre un botón Me gusta y el número de me gusta. Cuando el usuario pulse el botón, el número de Me gusta aumentará en uno.
Este es el código de nuestro componente:
import
React
,
{
useState
}
from
"react"
;
function
LikeButton
()
{
const
[
likes
,
setLikes
]
=
useState
(
0
);
function
handleLike
()
{
setLikes
(
likes
+
1
);
}
return
(
<
div
>
<
button
onClick
=
{
handleLike
}>
Like
</
button
>
<
p
>{
likes
}
Likes
</
p
>
</
div
>
);
}
export
default
LikeButton
;
En este código, hemos utilizado el gancho useState
para crear una variable de estado likes
, que contiene el número de me gusta. Para recapitular lo que ya sabemos sobre React, un gancho es una función especial que nos permite utilizar características de React, como métodos de estado y de ciclo de vida, dentro de componentes funcionales. Los ganchos nos permiten reutilizar la lógica de estado sin cambiar la jerarquía de los componentes, lo que facilita extraer y compartir ganchos entre componentes o incluso con la comunidad como paquetes autónomos de código abierto.
También hemos definido una función handleLike
que aumenta en uno el valor de los Me gusta cuando se pulsa el botón. Por último, mostramos el botón "Me gusta" y el número de "Me gusta" mediante JSX.
Ahora, veamos más de cerca cómo funciona el DOM virtual en este ejemplo.
Cuando el componente LikeButton
se renderiza por primera vez, React crea un árbol DOM virtual que refleja el árbol DOM real. El DOM virtual contiene un único elemento div
que contiene un elemento button
y un elementop
:
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'div'
,
props
:
{},
children
:
[
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'button'
,
props
:
{
onClick
:
handleLike
},
children
:
[
'Like'
]
},
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'p'
,
props
:
{},
children
:
[
0
,
' Likes'
]
}
]
}
La propiedad children
del elemento p
contiene el valor de la variable de estado Likes
, que inicialmente se pone a cero.
Cuando el usuario pulsa el botón Me gusta, se llama a la función handleLike
, que actualiza la variable de estado likes
. React crea entonces un nuevo árbol DOM virtual que refleja el estado actualizado:
{
type
:
'div'
,
props
:
{},
children
:
[
{
type
:
'button'
,
props
:
{
onClick
:
handleLike
},
children
:
[
'Like'
]
},
{
type
:
'p'
,
props
:
{},
children
:
[
1
,
' Likes'
]
}
]
}
Observa que el árbol DOM virtual contiene los mismos elementos que antes, pero la propiedad children
del elemento p
se ha actualizado para reflejar el nuevo valor de likes, pasando de 0
a 1
. Lo que sigue a es un proceso llamado reconciliación en React, en el que el nuevo vDOM se compara con el antiguo. Analicemos brevemente este proceso.
Tras calcular un nuevo árbol DOM virtual, React realiza un proceso llamado reconciliación para comprender las diferencias entre el nuevo árbol y el antiguo. La reconciliación es el proceso de comparar el antiguo árbol virtual DOM con el nuevo árbol virtual DOM y determinar qué partes del DOM real deben actualizarse. Si te interesa saber cómo se hace esto exactamente, el Capítulo 4 entra en muchos detalles al respecto. Por ahora, consideremos nuestro botón Me gusta.
En nuestro ejemplo, React compara el antiguo árbol virtual DOM con el nuevo árbol virtual DOM y descubre que el elemento p
ha cambiado: concretamente que sus props o su estado, o ambos, han cambiado. Esto permite a React marcar el componente como "sucio" o "debe actualizarse". A continuación, React calcula un conjunto mínimo efectivo de actualizaciones a realizar en el DOM real para reconciliar el estado del nuevo vDOM con el DOM, y finalmente actualiza el DOM real para reflejar los cambios realizados en el DOM virtual.
React actualiza sólo las partes necesarias del DOM real para minimizar el número de manipulaciones del DOM. Este enfoque es mucho más rápido y eficiente que actualizar todo el árbol DOM cada vez que se produce un cambio.
El DOM virtual ha sido un invento poderoso e influyente para la web moderna, y bibliotecas más recientes como Preact e Inferno lo adoptaron una vez que se probó en React. Trataremos más sobre el DOM virtual en el Capítulo 4, pero por ahora, pasemos a la siguiente sección.
El modelo de componentes
React fomenta mucho el "pensamiento en componentes": es decir, dividir tu aplicación en piezas más pequeñas y añadirlas a un árbol más grande para componer tu aplicación. El modelo de componentes es un concepto clave en React, y es lo que hace que React sea tan potente. Hablemos de por qué:
-
Fomenta la reutilización de lo mismo en todas partes, de modo que si se rompe, lo arreglas en un sitio y se arregla en todas partes. Esto se llama desarrollo DRY (Don't Repeat Yourself) y es un concepto clave en la ingeniería de software. Por ejemplo, si tenemos un componente
Button
, podemos utilizarlo en muchos lugares de nuestra aplicación, y si necesitamos cambiar el estilo del botón, podemos hacerlo en un lugar y se cambia en todas partes. -
React es más capaz de realizar un seguimiento de los componentes y hacer magia de rendimiento, como memoización, procesamiento por lotes y otras optimizaciones, si es capaz de identificar componentes específicos una y otra vez y realizar un seguimiento de las actualizaciones de a los componentes específicos a lo largo del tiempo. A esto se le llama keying. Por ejemplo, si tenemos un componente
Button
, podemos darle un propkey
y React podrá hacer un seguimiento del componenteButton
a lo largo del tiempo y "saber" cuándo actualizarlo, o cuándo saltárselo y seguir haciendo cambios mínimos en la interfaz de usuario. La mayoría de los componentes tienen claves implícitas, pero también podemos proporcionarlas explícitamente si queremos. -
Nos ayuda a separar las preocupaciones y a colocar la lógica más cerca de las partes de la interfaz de usuario a las que afecta. Por ejemplo, si tenemos un componente
RegisterButton
, podemos colocar la lógica de lo que ocurre cuando se pulsa el botón en el mismo archivo que el componenteRegisterButton
, en lugar de tener que saltar a diferentes archivos para encontrar la lógica de lo que ocurre cuando se pulsa el botón. El componenteRegisterButton
envolvería un componenteButton
más sencillo, y el componenteRegisterButton
se encargaría de manejar la lógica de lo que ocurre cuando se hace clic en el botón. A esto se le llama composición.
El modelo de componentes de React es un concepto fundamental que sustenta la popularidad y el éxito del framework. Este enfoque del desarrollo tiene numerosas ventajas, como una mayor modularidad, una depuración más sencilla y una reutilización del código más eficiente.
Estado inmutable
La filosofía de diseño de React hace hincapié en un paradigma en el que el estado de nuestra aplicación se describe como un conjunto de valores inmutables . Cada actualización de estado se trata como una instantánea y una referencia de memoria nuevas y distintas. Este enfoque inmutable de la gestión del estado es una parte esencial de la propuesta de valor de React, y tiene varias ventajas para desarrollar interfaces de usuario robustas, eficientes y predecibles.
Al imponer la inmutabilidad, React garantiza que los componentes de la interfaz de usuario reflejen un estado específico en un momento dado. Cuando el estado cambia, en lugar de mutarlo directamente, devuelve un nuevo objeto que representa el nuevo estado. Esto facilita el seguimiento de los cambios, la depuración y la comprensión del comportamiento de tu aplicación. Comolas transiciones de estado son discretas y no interfieren entre sí, se reducen significativamente las posibilidades de que se produzcan errores sutiles causados por un estado mutable compartido.
En próximos capítulos, exploraremos cómo React agrupa por lotes las actualizaciones de estado y las procesa de forma asíncrona para optimizar el rendimiento. Dado que el estado debe tratarse de forma inmutable, estas "transacciones" pueden agregarse y aplicarse de forma segura sin riesgo de que una actualización corrompa el estado de otra. Esto conduce a una gestión del estado más predecible y puede mejorar el rendimiento de la app, especialmente durante transiciones de estado complejas.
El uso de estado inmutable refuerza aún más las buenas prácticas en el desarrollo de software. Anima a los desarrolladores a pensar funcionalmente sobre su flujo de datos, reduciendo los efectos secundarios y haciendo que el código sea más fácil de seguir. La claridad de un flujo de datos inmutable simplifica el modelo mental para entender cómo funciona una aplicación.
La inmutabilidad también permite potentes herramientas de desarrollo, como depuración de viajes en el tiempo con herramientas como Replay.io, donde los desarrolladores pueden avanzar y retroceder a través de los cambios de estado de una aplicación para inspeccionar la interfaz de usuario en cualquier momento. Esto sólo es factible si cada actualización de estado se mantiene como una instantánea única y sin modificar.
El compromiso de React con las actualizaciones de estado inmutables es una elección de diseño deliberada que aporta numerosas ventajas. Se alinea con los principios de la programación funcional moderna, permitiendo actualizaciones eficientes de la interfaz de usuario, optimizando el rendimiento, reduciendo la probabilidad de errores y mejorando la experiencia general del desarrollador. Este enfoque de la gestión de estados sustenta muchas de las funciones avanzadas de React y seguirá siendo una piedra angular a medida que React evolucione.
Liberar React
El flujo unidireccional de datos era un cambio radical respecto a la forma en que habíamos estado construyendo aplicaciones web durante años, y fue recibido con escepticismo. El hecho de que Facebook fuera una gran empresa con muchos recursos, muchos usuarios y muchos ingenieros con opiniones hizo que su ascenso fuera empinado. Tras mucho escrutinio, React fue un éxito interno. Fue adoptado por Facebook y luego por Instagram.
En 2013 se convirtió en código abierto y se lanzó al mundo, donde se encontró con una enorme cantidad de reacciones en contra. La gente criticó duramente a React por su uso de JSX, acusando a Facebook de "poner HTML en JavaScript" y romper la separación de preocupaciones. Facebook pasó a ser conocida como la empresa que "replantea las buenas prácticas" y rompe la web. Finalmente, tras una adopción lenta y constante por parte de empresas como Netflix, Airbnb yThe New York Times, React se convirtió en el estándar de facto para construir interfaces de usuario en la web.
Se omiten algunos detalles de esta historia porque quedan fuera del alcance de este libro, pero es importante comprender el contexto de React antes de sumergirnos en los detalles: concretamente, la clase de problemas técnicos para cuya resolución se creó React. Si te interesa más lahistoria de React, hay un documental completo sobre lahistoria de React que está disponible gratuitamente en YouTube bajo el título React.js: The Documentary by Honeypot.
Dado que Facebook tenía un asiento en primera fila para estos problemas a enorme escala, React fue pionero en un enfoque basado en componentes para construir interfaces de usuario que resolvieran estos problemas y más, donde cada componente sería una unidad autónoma de código que podría reutilizarse y componerse con otros componentes para construir interfaces de usuario más complejas.
Un año después del lanzamiento de React como software de código abierto, Facebook publicó Flux: un patrón para gestionar el flujo de datos en aplicaciones React. Flux fue una respuesta a los retos de gestionar el flujo de datos en aplicaciones a gran escala, y fue una parte clave del ecosistema React. Echemos un vistazo a Flux y cómo encaja en el ecosistema React.
La Arquitectura Flux
Flux es un patrón de diseño arquitectónico para construir aplicaciones web del lado del cliente, popularizado por Facebook (ahora Meta) (ver Figura 1-3). Hace hincapié en un flujo de datos unidireccional, lo que hace que el flujo de datos dentro de la aplicación sea más predecible.
He aquí los conceptos clave de la arquitectura Flux:
- Acciones
-
Acciones son objetos simples que contienen datos nuevos y una propiedad de tipo identificativo. Representan las entradas externas e internas al sistema, como las interacciones del usuario, las respuestas del servidor y las entradas de formularios. Las acciones se envían a través de un despachador central a varios almacenes:
// Example of an action object
{
type
:
'ADD_TODO'
,
text
:
'Learn Flux Architecture'
}
- Despachador
-
El despachador es el eje central de la arquitectura Flux. Recibe acciones y las envía a los almacenes registrados en la aplicación. Gestiona una lista de retrollamadas, y cada tienda se registra a sí misma y a su retrollamada en el despachador. Cuando se envía una acción, se envía a todas las llamadas de retorno registradas:
// Example of dispatching an action
Dispatcher
.
dispatch
(
action
);
- Tiendas
-
Almacenes contienen el estado y la lógica de la aplicación. Son algo similares a los modelos de la arquitectura MVC, pero gestionan el estado de muchos objetos. Se registran con el despachador y proporcionan retrollamadas para gestionar las acciones. Cuando se actualiza el estado de un almacén, emite un evento de cambio para avisar a las vistas de que algo ha cambiado:
// Example of a store
class
TodoStore
extends
EventEmitter
{
constructor
()
{
super
();
this
.
todos
=
[];
}
handleActions
(
action
)
{
switch
(
action
.
type
)
{
case
"ADD_TODO"
:
this
.
todos
.
push
(
action
.
text
);
this
.
emit
(
"change"
);
break
;
default
:
// no op
}
}
}
- Vistas
-
Vistas son componentes React. Escuchan los eventos de cambio de los almacenes y se actualizan cuando cambian los datos de los que dependen. También pueden crear nuevas acciones para actualizar el estado del sistema, formando un ciclo unidireccional de flujo de datos.
La arquitectura Flux promueve un flujo de datos unidireccional a través de un sistema, lo que facilita el seguimiento de los cambios a lo largo del tiempo. Esta previsibilidad puede utilizarse posteriormente como base para que los compiladores optimicen aún más el código, como ocurre con React Forget (más sobre esto más adelante).
Ventajas de la Arquitectura Flux
La arquitectura Flux aporta una serie de ventajas que ayudan a gestionar la complejidad y mejorar la mantenibilidad de las aplicaciones web. He aquí algunas de las ventajas más destacadas:
- Fuente única de la verdad
-
Flux hace hincapié en tener una única fuente de verdad para el estado de la aplicación, que se almacena en los almacenes. Esta gestión centralizada del estado hace que el comportamiento de la aplicación sea más predecible y fácil de entender. Elimina las complicaciones que conlleva tener múltiples fuentes de verdad interdependientes, que pueden dar lugar a errores y a un estado incoherente en toda la aplicación.
- Comprobabilidad
-
Las estructuras bien definidas y el flujo de datos predecible de Flux hacen que la aplicación sea altamente comprobable. La separación de preocupacionesentre las distintas partes del sistema (como acciones, despachador, almacenes y vistas) permite realizar pruebas unitarias de cada parte de forma aislada. Además, es más fácil escribir pruebas cuando el flujo de datos es unidireccional y cuando el estado se almacena en ubicaciones específicas y predecibles.
- Separación de intereses (SoC)
-
Flux separa claramente las preocupaciones de las distintas partes del sistema, como se ha descrito anteriormente. Esta separación hace que el sistema sea más modular, más fácil de mantener y más fácil de razonar. Cada parte tiene una función claramente definida, y el flujo unidireccional de datos deja claro cómo interactúan entre sí.
La arquitectura Flux proporciona una base sólida para construir aplicaciones web robustas, escalables y mantenibles. Su énfasis en un flujo de datos unidireccional, una única fuente de verdad y la Separación de Intereses conduce a aplicaciones más fáciles de desarrollar, probar y depurar .
Conclusión: ¿Por qué existe React?
React es una cosa porque permite a los desarrolladores construir interfaces de usuario con mayor previsibilidad y fiabilidad, permitiéndonos expresar de forma declarativa lo que nos gustaría en la pantalla mientras React se encarga del cómo, haciendo actualizaciones incrementales del DOM de forma eficiente. También nos anima a pensar en componentes, lo que nos ayuda a separar las preocupaciones y a reutilizar el código más fácilmente. Está probado en Meta y diseñado para ser utilizado a escala. También es de código abierto y de uso gratuito.
React también tiene un vasto y activo ecosistema, con una amplia gama de herramientas, bibliotecas y recursos a disposición de los desarrolladores. Este ecosistema incluye herramientas para probar, depurar y optimizar aplicaciones React, así como bibliotecas para tareas comunes como la gestión de datos, el enrutamiento y la gestión de estados. Además, la comunidad React es muy comprometida y solidaria, con muchos recursos en línea, foros y comunidades disponibles para ayudar a los desarrolladores a aprender y crecer.
React es agnóstico en cuanto a plataformas, lo que significa que puede utilizarse para crear aplicaciones web para una amplia gama de plataformas, como escritorio, móvil y realidad virtual. Esta flexibilidad hace de React una opción atractiva para los desarrolladores que necesitan crear aplicaciones para varias plataformas, ya que les permite utilizar un único código base para crear aplicaciones que se ejecuten en varios dispositivos.
En conclusión, la propuesta de valor de React se centra en su arquitectura basada en componentes, modelo de programación declarativo, DOM virtual, JSX, amplio ecosistema, naturaleza agnóstica de la plataforma y respaldo de Meta. Juntas, estas características hacen de React una opción atractiva para los desarrolladores que necesitan crear aplicaciones web rápidas, escalables y mantenibles. Tanto si estás construyendo un sitio web sencillo como una aplicación empresarial compleja, React puede ayudarte a alcanzar tus objetivos de forma más eficiente y eficaz que muchas otras tecnologías. Repasemos.
Revisión de capítulos
En este capítulo, hemos cubierto una breve historia de React, su propuesta de valor inicial y cómo resuelve los problemas de las actualizaciones inseguras, impredecibles e ineficaces de la interfaz de usuario a escala. También hemos hablado del modelo de componentes y de por qué ha sido revolucionario para las interfaces en la web. Recapitulemos lo que hemos aprendido. Lo ideal es que después de este capítulo estés más informado sobre las raíces de React y de dónde viene, así como sobre sus principales puntos fuertes y su propuesta de valor.
Preguntas de repaso
Asegurémonos de que has comprendido bien los temas que hemos tratado. Tómate un momento para responder a las siguientes preguntas:
-
¿Cuál fue la motivación para crear React?
-
¿Cómo mejora React patrones anteriores como MVC y MVVM?
-
¿Qué tiene de especial la arquitectura Flux?
-
¿Cuáles son las ventajas de las abstracciones de la programación declarativa?
-
¿Qué papel desempeña el DOM virtual en la actualización eficaz de la interfaz de usuario?
Si tienes problemas para responder a estas preguntas, puede que merezca la pena volver a leer este capítulo. Si no, exploremos el siguiente capítulo.
Siguiente
En el Capítulo 2 profundizaremos un poco más en esta abstracción declarativa que nos permite expresar lo que queremos ver en la pantalla: la sintaxis y el funcionamiento interno de JSX, el lenguaje que parece HTML en JavaScript que metió a React en muchos problemas en sus inicios, pero que finalmente se reveló como la forma ideal de construir interfaces de usuario en la web, influyendo en una serie de futuras bibliotecas para construir interfaces de usuario.
Get React fluido now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.