Cómo jugar Jenga con códigos: migrar una aplicación compleja de AngularJS a React

by Juan Cabello
10 Minute Read

El estrés de tomar un solo ladrillo de la torre y colocarlo en la parte superior; la intensidad cada vez mayor con cada movimiento hecho; toda persona que haya jugado Jenga entiende esta sensación. Imagina jugar Jenga con una torre enorme frente a un público en vivo, donde cualquier paso en falso implica un desastre total. La experiencia de mi equipo de migrar una aplicación de AngularJS a React se ha sentido como un desafío similar.

Imagina una aplicación de front-end como la torre de Jenga y cada ladrillo como un componente interdependiente. Quitar un ladrillo podría significar corregir un error en un código heredado, mientras que reemplazar un ladrillo representaría programar una nueva característica. Los usuarios de la aplicación no deberían notar ningún cambio, excepto un diseño potencialmente actualizado, y la aplicación debería funcionar, como mínimo, tan bien como antes. Dado que ambos marcos son de JavaScript, esto no debería ser un gran desafío, ¿verdad?

Honestamente, esta migración sí nos ha parecido un gran desafío. No es fácil planificar la migración, desarrollar nuevas características en nuestra hoja de ruta de productos y saldar la deuda tecnológica; pero tuvimos que enfrentar todos estos desafíos simultáneamente.

A pesar de estos obstáculos, decidimos que era hora de pasar de AngularJS a React.

Razones para este cambio

AngularJS marcó un nuevo comienzo en el desarrollo front-end y demostró que implica más que solo trabajar en la interfaz de usuario (UI). Sin embargo, los niveles de abstracción de AngularJS eran demasiado complicados para la mayoría de los ingenieros de front-end. No se podía escribir sencillamente una función de jQuery para cambiar la visibilidad de los elementos; ahora, se necesitaba una ingeniería adecuada, desde actualizar el DOM basado en estados hasta gestionar dependencias. (Recuerda que esto era antes de los module bundlers).

Desde la publicación de AngularJS, muchos marcos de interfaz de usuario y bibliotecas se han hecho más populares. Las dificultades de AngularJS se volvieron engorrosas, por lo que los desarrolladores de front-end optaron por nuevos marcos. Además, la última versión de AngularJS que ofrecía soporte a largo plazo (LTS) de tres años fue la 1.7, que se lanzó en julio de 2018. Los productos serios no se pueden construir en marcos sin soporte a largo plazo, así que esta era una señal de que necesitábamos cambiar los marcos pronto.

Elegimos React principalmente porque los ingenieros de front-end lo adoptaron y era fácil contratar experimentados en React. También funciona mejor que Angular. El ecosistema de React es enorme y ya era más grande que el de Angular cuando decidimos hacer el cambio.

Después de la decisión de migrar, tuvimos que averiguar por dónde empezar. Buscamos historias de otras empresas que habían migrado; para nuestra sorpresa, había poca información. Así que, abrimos nuestro propio camino.

La ingeniería de front-end consiste en mostrar bits de datos optimizados para que a los usuarios les parezcan amigables, al mismo tiempo que se resuelven sus problemas. Ya sea que se interactúe con otro servicio o que presente información sobre un producto, el propósito principal de un front-end es permitir a los usuarios interactuar con los bits de datos mostrados. La capacidad de resolución de problemas de un producto se puede medir según tres factores: (1) el tiempo que un usuario pasa ejecutando una tarea específica; (2) la disponibilidad del servicio; y (3) la congruencia de los datos. La experiencia del usuario (UX) combina la efectividad de un producto con la sensación que obtiene el usuario al interactuar con él.

Diferentes arquitecturas

Si tienes deuda tecnológica acumulada en tu código de AngularJS, no debes llevarla a la nueva base de código de React. Tampoco es una buena idea transportar errores que no se hayan resuelto. Para un buen comienzo, debes empezar por los conceptos básicos: la arquitectura.

Para comenzar a desarrollar una arquitectura adecuada, tuvimos que entender las diferencias entre desarrollar una aplicación en AngularJS y en React. El primer gran problema que abordamos fue que AngularJS genera una dependencia del proveedor, y React no. (El lema de React es «Una biblioteca de JavaScript para construir interfaces de usuario»). En el mundo de Angular, debes seguir el estilo de Angular y usar sus herramientas para las llamadas de API, los estados y mucho más. Podrías optar por no hacerlo, pero te enfrentarías con problemas como la actualización de la interfaz de usuario si los datos recibidos no sucedieron en la misma pila de llamadas (por ejemplo, callbacks, promesas, etc.). Además, hay mucha jerga técnica que aprender con Angular, como controladores, directivas, módulos, servicios, etc. Para gestionar de forma efectiva todo esto, AngularJS recomienda descargar las operaciones de gestión de estado al controlador. El estado es recuperado por los servicios. Los controladores son como el pegamento que recupera el estado de un servicio, gestiona el estado y actualiza la vista. La vista en AngularJS está compuesta de plantillas que muestran el estado actual según lo reportado por los controladores.

En React, lo más importante es entender el flujo de datos. Basta con imaginar una aplicación de React como un árbol de datos en el que cada nodo es un componente (o ladrillo de Jenga). Cada componente tiene un estado gestionado por su componente respectivo y recibe propiedades conocidas como props, que son administradas por un componente padre. Este sistema regula la forma en que los datos fluyen en la aplicación. Las props representan los bordes entre los nodos, mientras que el estado encapsula los datos y está representado por los nodos en sí. El estado puede transmitirse en el árbol a través de las props o puede usarse para ese componente en particular. Otra opción es crear «contexto», que crea datos compartidos entre nodos que están suscritos a ese contexto.

Si quieres conservar el estilo arquitectónico de Angular, puedes usar patrones de diseño análogos en React. Por ejemplo, puedes crear diferentes componentes sin estado similares a las plantillas de Angular, pero que simplemente muestren datos y manejen eventos. Para reemplazar los controladores de Angular, puedes usar un componente con estado que administre los datos recibidos por un servidor, contexto, su estado local o props de componentes superiores y los suministre a varios componentes sin estado.

Si quieres hacer las cosas al estilo de React, puedes mejorar tu aplicación con una biblioteca centralizada de contenedores de estado, como Redux.

Migración por capas

Ahora conocemos las diferencias entre el funcionamiento de cada marco, pero ¿hay algo común que podemos aprovechar para facilitar nuestra migración? En las aplicaciones web modernas, a menudo hay tres capas, todas ellas dentro de un mismo archivo modular. Por un lado, la capa de estado, que maneja principalmente la comunicación con el servidor y actúa como una capa de almacenamiento en caché. Luego está la capa del controlador, que administra las transiciones de estado y la comunicación con la capa de estado. Por último, la capa de vista, que muestra el estado actual de la aplicación.

Para planificar una migración, debes tener en cuenta las tres capas de la aplicación. Es importante especialmente cuando migras una aplicación para la cual no se desarrolló una arquitectura eficiente; es un buen lugar para comenzar a hacer las cosas bien. Contemplamos tres enfoques diferentes para migrar una aplicación de AngularJS:

  1. Migrar la capa de estado primero, luego las capas de controlador y vista
  2. Migrar la capa de vista primero, luego las capas de estado y controlador
  3. Migrar las tres capas a la vez en pasos continuos

Analicemos cada uno de los enfoques mencionados.

Migrar la capa de estado primero, luego las capas de controlador y vista

Este enfoque se centra primero en reemplazar los servicios de AngularJS. Para este fin, un contenedor de estado, como Redux, Flux o MobX, es una buena manera de centralizar todas las operaciones de datos y de desacoplar la capa de estado de cualquier otra capa. Asegúrate de escoger lo que mejor se adapte a tus necesidades; hay muchos recursos que describen cada contenedor de estado. Al centralizar todas las operaciones de almacenamiento y descargarlas de AngularJS, también logramos la libertad completa para la interfaz de usuario. Evitas depender de una biblioteca de interfaz de usuario, lo cual hace que sea más fácil reemplazarla con lo que necesites. En este caso, solo podemos reemplazar los controladores, las directivas y las plantillas de AngularJS con componentes de React hasta que finalmente eliminemos por completo AngularJS de nuestra base de código.

Migrar la capa de vista primero, luego las capas de estado y controlador

Este enfoque se centra primero en las plantillas de AngularJS. Dado que las plantillas de AngularJS son principalmente HTML y CSS, es bastante fácil ponerlas en un componente de React mediante enlaces adecuados para conectar el componente de React a tus controladores de Angular. Las bibliotecas como react2angular o ngReact te permiten vincular los componentes de React directamente a tus plantillas de Angular. Quizá enfrentes problemas de rendimiento; pero, en general, el rendimiento no se ve significativamente afectado. También es posible lograr una arquitectura desacoplada de esta manera, ya que solo se está invirtiendo el orden de la migración del primer enfoque. Los controladores son el pegamento, así que, de cualquier manera, este debe ser el último paso en cualquiera de los dos casos. Este último paso es el mismo que el primero del primer enfoque que presentamos.

Migrar las tres capas a la vez en pasos continuos

El tercer y último enfoque que contemplamos se centra en partes de la aplicación y tiene como objetivo migrarlas verticalmente. Puedes imaginarlo de la siguiente manera: las páginas que se renderizan de forma independiente también se migrarán de forma independiente. Es tentador seguir este enfoque, pero también es muy fácil diseñar tu sistema de manera tal que se cambie continuamente. Además, el rendimiento puede verse afectado mientras la migración está en curso. Al migrar una vista renderizada, hay usos de estado ocultos que podrían no ser considerados, y al final, eso exige mantener el estado en dos partes diferentes de tu aplicación.

Lo que elegimos

En realidad, usamos los tres enfoques en nuestra migración. Nuestros pasos iniciales fueron migrar componentes muy pequeños siguiendo el segundo enfoque; es decir, todavía estábamos utilizando el controlador y la capa de estado de Angular, pero implementábamos la interfaz de usuario en React. Llegamos a la conclusión de que de esta manera estábamos prolongando el pago de la deuda tecnológica, lo cual después podía ser contraproducente. No se aprovechó la oportunidad de hacer las cosas correctamente en esta ocasión; pero aprendimos de eso. Adquirimos experiencia de primera mano con las bibliotecas que más tarde se reutilizarían en los otros enfoques que tomamos.

Volvimos a evaluar y optamos por la tercera opción. Migramos nuestra interfaz de usuario con vistas prerrenderizadas. En promedio, invertimos un mes en cada vista completa, incluida la reescritura de todas las interacciones, la migración del estado a Redux y etc. Avanzamos y pudimos llevar una aplicación a la otra sin demasiadas complicaciones. Sin embargo, nos dimos cuenta de que este enfoque tardaba demasiado tiempo. Estábamos comprometiendo nuestro plazo de entrega y aumentando el riesgo de generar una mala calidad de código debido a la presión del tiempo. Por lo tanto, después de migrar dos páginas completas, nos detuvimos y volvimos a evaluar.

Queríamos lograr tiempos de entrega más rápidos y la capacidad de analizar nuestros pasos para que nuestros recursos de ingeniería pudieran dividirse en diferentes sprints sin perder impulso. Además, queríamos aprovechar esta oportunidad para hacer las cosas correctamente y pagar la deuda tecnológica a medida que avanzábamos, o al menos no llevar la deuda tecnológica a nuestra nueva base de código.

En general, estuvimos más satisfechos al migrar primero el estado y creemos que se volverá cada vez más importante cuando comencemos a migrar nuestro componente más fundamental y más cargado de datos. Recuerda: el front-end consiste en mostrar bits de datos. Así que, al asegurarte de que los datos no dependan de los marcos, puedes empezar a concentrarte completamente en optimizar la vista para tus usuarios como la manera más efectiva de resolver el problema.

Conclusión

Hemos adquirido aprendizajes útiles para cualquiera que migre una aplicación de AngularJS a React y para cualquiera que haga algún tipo de migración de front-end. Estos son algunos de los más importantes:

  • La buena arquitectura ayuda a saldar la deuda tecnológica

Si no diseñas correctamente la arquitectura de tu aplicación, será fácil migrar también la deuda tecnológica. Aunque la inversión de tiempo ya es alta, recomendamos resolver la deuda tecnológica durante la migración, en lugar de acumularla, ya que los costos futuros de resolver la deuda tecnológica luego son aún más altos. Comienza por entender la arquitectura básica de una aplicación de React y una aplicación de AngularJS y, luego, entiende qué enfoque seguir.

  • Considera la migración una característica

Migrar una aplicación te da la oportunidad de corregir lo que estaba mal. No desaproveches la oportunidad y ayuda a tu equipo a planificar el desarrollo del producto como corresponde. Puede llevar más tiempo, pero una buena corrección retribuirá el tiempo que invertiste a medida que tu producto crece.