Preparando el terreno: alto riesgo y alta escala
En Sardine, nuestro dashboard es la interfaz principal de nuestra plataforma de detección de fraude y cumplimiento. Analistas de cumplimiento, operadores de riesgo y equipos de ingeniería lo usan a diario para investigar transacciones, gestionar reglas y monitorear alertas. Es un monorepo grande de React y TypeScript con cientos de componentes, decenas de paquetes internos e integraciones profundas con servicios backend.
React 18 trajo mejoras significativas, como el batching automático, funciones concurrentes y una API de renderizado modernizada. Sin embargo, para una aplicación en producción a nuestra escala, la actualización requirió más que un simple cambio de versión.
Los cambios de comportamiento entre React 17 y 18 son lo suficientemente sutiles como para pasar pruebas automatizadas y QA manual. A menudo solo se manifiestan bajo condiciones de datos específicas en producción; por ejemplo, cuando un cliente reporta una pantalla congelada causada por un bucle infinito de re-renderizado. Estos problemas son difíciles de encontrar y aún más difíciles de solucionar después del hecho. Esta realidad definió nuestra estrategia: necesitábamos un enfoque que nos permitiera validar cambios de forma incremental con tráfico real, manteniendo la capacidad de revertir al instante. Se consideró un único PR grande con una sesión dedicada de caza de errores, pero una actualización monolítica es difícil de revertir, difícil de bisectar cuando surgen problemas, y difícil de solucionar bajo presión de tiempo.
Este artículo cubre la arquitectura que construimos para resolver estos desafíos: un cambio de versión de React por usuario, controlado por feature flags y basado en mapas de importación. También compartimos los codemods y las reglas personalizadas de ESLint que escribimos para resolver los problemas de comportamiento descubiertos durante el lanzamiento.
Nota: Los fragmentos de código a lo largo de este artículo están simplificados para mayor claridad y no representan la implementación exacta en producción.
El desafío técnico: cambios sutiles y fallos silenciosos
React 18 introduce cambios que parecen simples sobre el papel pero se comportan de forma impredecible en bases de código grandes. Dos problemas principales destacaron durante nuestra migración:
El batching automático cambia cuándo se aplican las actualizaciones de estado
En React 17, las actualizaciones dentro de promesas o timeouts activaban un re-renderizado por cada setState. React 18 agrupa estas en una sola pasada. Aunque generalmente es una mejora de rendimiento, el código que lee el DOM o una ref inmediatamente después de una llamada a setState, esperando un renderizado intermedio, ahora verá datos "obsoletos" porque el renderizado aún no se ha aplicado. Esto falla en silencio; la interfaz eventualmente se ve correcta, pero la lógica intermedia está rota.
La igualdad referencial más estricta expone referencias inestables
La reconciliación de React 18 es más agresiva a la hora de evitar re-renderizados cuando las props no han cambiado. Este es el comportamiento correcto, pero expone una clase de errores que React 17 toleraba silenciosamente: valores en línea como [], {}, y () => {} usados como valores predeterminados crean nuevas referencias en cada renderizado. Esto activa bucles infinitos de re-renderizado cuando son consumidos por useEffect, useMemo, o bibliotecas como @tanstack/react-table que dependen de la igualdad referencial.
Sabíamos que estos problemas serían casi imposibles de detectar exhaustivamente antes del lanzamiento. Por eso no lo tratamos como un simple cambio de versión; lo tratamos como un lanzamiento controlado, con infraestructura diseñada para encontrar y solucionar problemas de forma incremental.
Nota: Como nuestro dashboard es una SPA pura sin renderizado del lado del servidor, evitamos por completo la aplicación más estricta de discrepancias de hidratación de React 18 (si tu aplicación usa SSR, espera una ola de errores de hidratación durante la migración).
Eligiendo el camino correcto: de los codemods al cambio a nivel de infraestructura
Antes de construir una solución personalizada, evaluamos herramientas y arquitecturas existentes para encontrar el camino de lanzamiento más seguro posible.
Herramientas oficiales de migración
El equipo de React ofrece react-codemod, un conjunto de scripts de migración automatizados. Ejecutamos las transformaciones relevantes: actualizando métodos de ciclo de vida obsoletos, migrando definiciones de tipos (eliminando React.VFC en favor de React.FC donde también eliminó los children implícitos, actualizando los tipos de retorno de render), y aplicando el codemod update-react-imports para la nueva transformación JSX. Estos manejaron los cambios mecánicos a nivel de API y fueron un primer paso necesario.
Pero los codemods solo pueden corregir lo que es identificable sintácticamente. Los problemas de comportamiento (referencias inestables, cambios de batching) no son problemas de sintaxis. Son problemas de runtime que dependen de cómo interactúan los componentes entre sí y con los datos. Ningún codemod puede detectar que tu useEffect entrará en un bucle infinito bajo una combinación específica de props. Para eso, necesitábamos una forma de ejecutar la nueva versión contra tráfico real.
Estrategias de cambio de versión
Consideramos tres enfoques y los hemos listado en la tabla a continuación:
Enfoque | Ventajas | Desventajas |
Mapas de importación + CDN | No requiere reconstrucciones; permite reversiones instantáneas y segmentación granular por usuario. | Introduce dependencias de CDN/ESM estático y un ligero aumento de latencia al cargar React. |
Aliases duales de npm | Funciona por completo sin dependencia de CDN. | Requiere artefactos de compilación duales, lo que resulta en paquetes más grandes y una sobrecarga significativa de infraestructura. |
Ofrece aislamiento completo de procesos. | Requisitos de infraestructura pesados, excesivo para nuestra configuración. |
Los mapas de importación nos dieron la mayor flexibilidad con la menor sobrecarga de infraestructura. Cambiar de versión de React es una sustitución de cadena en el servidor. Sin redespliegue, sin pipeline de compilación separado, sin contenedores adicionales. La única salvedad es la compatibilidad de navegadores, ya que los mapas de importación son un estándar web relativamente reciente. Antes de comprometernos con este enfoque, cruzamos nuestros datos de analítica con Can I use y confirmamos que el 100% de nuestra base de usuarios activos usa navegadores con compatibilidad total. Como producto B2B usado por equipos de cumplimiento y riesgo, nuestro panorama de navegadores es predecible y actualizado, lo que hizo de esta una apuesta segura.

La arquitectura: cambio de versión de activos de biblioteca remotos
En lugar de empaquetar React dentro de nuestra aplicación, lo externalizamos en las compilaciones de producción. El servidor verifica un feature flag e inyecta el mapa de importación apropiado en index.html antes de servirlo.
Como el mapa de importación se genera en el servidor, podemos controlar la fuente del activo por entorno sin fricción alguna. Si lo necesitáramos, podríamos servir React desde nuestro propio bucket de S3 sin cambios de código.

Primero, externalizamos React, para que no esté incluido en el paquete final:
En desarrollo, Vite empaqueta React 18 directamente, por lo que cada ingeniero trabaja contra la versión objetivo durante su flujo de trabajo normal.
La lógica del lado del servidor:
La plantilla HTML contiene un marcador de posición que se reemplaza con el mapa de importación.
Al hacer esto, el motor del navegador resolverá de forma nativa todos los import React from 'react', llamando al CDN.
Punto de entrada de renderizado dual
El punto de entrada de la aplicación detecta qué API de React está disponible en tiempo de ejecución:
Una estrategia de lanzamiento por fases

El lanzamiento siguió cinco fases, cada una controlada por el feature flag:
Fase 0: Pre-merge
Todo el código de migración se fusionó a main con el flag desactivado por defecto. Producción siguió ejecutando React 17 sin ningún cambio.
Fase 1: Desarrollo
Los ingenieros hicieron su trabajo diario contra React 18 localmente (Vite siempre empaqueta React 18 en desarrollo). Esto detectó problemas de forma temprana dentro del flujo normal de desarrollo.
Fase 2: Sandbox
Nuestro entorno Sandbox tiene dos propósitos: los clientes lo usan para evaluar nuestra plataforma, y cada lanzamiento se despliega ahí antes de producción. Habilitamos el flag aquí primero. Esto nos dio semanas de patrones de uso real; clientes ejecutando sus propios flujos de evaluación, sin ningún riesgo para producción.
Fase 3: Usuarios internos
Flag habilitado para empleados de Sardine en producción. Como el feature flag soporta segmentación por usuario y por organización, pudimos delimitar el lanzamiento con precisión: primero nuestro equipo de ingeniería, luego el resto de la empresa. Esto nos permitió ejercitar flujos de trabajo críticos con datos en vivo antes de que ningún cliente viera el cambio.
Fase 4: Lanzamiento gradual
Lanzamiento basado en porcentaje a usuarios externos, escalando del 5% al 100%. Como la versión de React se determina en el servidor en cada carga de página, no hay estado en caché que invalidar. Se cambia el flag, y la siguiente solicitud obtiene la versión anterior.
Lo que encontramos: referencias inestables a escala
Una vez que el lanzamiento llegó a los usuarios internos en el Entorno Local y Sandbox, empezamos a sufrir bucles infinitos de re-renderizado. Componentes que funcionaban perfectamente bajo React 17 congelaban el navegador bajo React 18. La causa raíz era siempre la misma clase de problema: referencias inestables.
Aquí tienes un ejemplo simplificado del patrón:
Cada vez que se ejecuta este hook, el valor predeterminado [] y () => {} crean nuevas referencias de objeto. Cualquier useEffect o useMemo que dependa de estos valores se vuelve a ejecutar, lo que puede activar otro renderizado, que crea nuevas referencias de nuevo, entrando en un bucle infinito.
React 17 toleraba esto porque su reconciliación era menos agresiva respecto a las verificaciones de referencia. React 18 expone estos errores de forma más agresiva debido a que el Modo Estricto invoca los efectos dos veces en desarrollo y a que el Renderizado Concurrente programa las actualizaciones de forma diferente. React 18 hace lo correcto, pero hacer lo correcto expuso años de atajos acumulados.
Constantes estables
La solución es un conjunto de referencias singleton congeladas:
Cuando se aplican, se ven así:
Esto era un concepto simple. El desafío fue aplicarlo de forma consistente en cientos de archivos. Y necesitábamos movernos rápido, porque estos problemas estaban bloqueando el lanzamiento final.
Codemods para transformación automatizada
Construimos dos codemods de jscodeshift para manejar el trabajo repetitivo:
fix-unstable-hook-returns escanea cada función que coincide con use[A-Z]* y reemplaza los valores de retorno que contienen [], {}, o () => {} con la constante estable correspondiente.
fix-unstable-destructuring-defaults detecta la variante de desestructuración:
Ambos codemods gestionan los imports automáticamente, agregando o extendiendo el import de @/react-migration-support según sea necesario.
Al final, después de que se ejecutaran todos los codemods, tuvimos más de 1,000 archivos modificados.
Reglas personalizadas de ESLint para prevenir regresiones
Si bien remediar el código base existente era necesario, prevenir regresiones durante el lanzamiento por fases era igualmente crítico. Para aplicar estos patrones automáticamente y garantizar la estabilidad a largo plazo de la migración, desarrollamos cinco reglas personalizadas de ESLint:
Regla | Qué detecta |
| Literales en línea como props predeterminadas de componentes. |
| Valores de retorno inestables de hooks personalizados. |
|
|
| Sin memoizar |
|
|
La regla no-unstable-react-table-props merece destacarse. @tanstack/react-table usa igualdad referencial internamente. Si los datos o las columnas no están memoizados, la tabla se vuelve a renderizar en cada actualización del padre. Esta regla rastrea las declaraciones de useMemo y useState y marca cualquier valor no memoizado pasado a useReactTable:
Estas reglas siguen siendo útiles mucho más allá de la migración. Los patrones que aplican son buenas prácticas independientemente de la versión de React, y detectarán la misma clase de problemas cuando eventualmente migremos a React 19.
Reflexiones finales: un plan para migraciones a gran escala
- Los mapas de importación están subestimados para la migración de versiones. Nos permitieron implementar el cambio de versión por usuario sin requerir infraestructura de compilación adicional. Aunque existe una dependencia externa de CDN, es manejable a través de proveedores confiables como jsDelivr o agregando un proxy de caché.
- Invierte en prevención, no solo en remediación. Si bien los codemods fueron esenciales para corregir el código existente en una sola pasada, nuestras reglas personalizadas de ESLint aseguran que esos mismos patrones nunca regresen. Estas reglas requirieron esfuerzo para escribirse, pero se pagan solas cada vez que detectan un error antes de un commit en lugar de como un reporte de bug en producción.
- Los feature flags transforman actualizaciones de alto riesgo en despliegues rutinarios. La capacidad de segmentar usuarios específicos, organizaciones, o porcentajes de tráfico nos dio control total sobre el lanzamiento. Esta granularidad, combinada con la capacidad de realizar una reversión instantánea, eliminó la presión de tiempo tradicional asociada con los grandes cambios de versión.
- Aborda los problemas en producción, de forma segura y sin presión de tiempo. Los problemas de referencias inestables que descubrimos habrían sido casi imposibles de detectar solo mediante pruebas aisladas. Nuestra arquitectura de mapas de importación creó un entorno seguro para encontrar estos casos límite usando tráfico y datos reales, respaldado por la red de seguridad de una reversión instantánea.
Mirando hacia adelante: preparándonos para React 19 y más allá
La belleza de la arquitectura de mapas de importación es que es completamente agnóstica a la versión. Cuando llegue React 19, esta misma infraestructura manejará el lanzamiento con el mismo nivel de precisión y seguridad.
El patrón que construimos, externalizar una dependencia central, cambiar de versión mediante mapas de importación, y controlar la transición con feature flags, no es específico de React. Proporciona un camino de lanzamiento repetible, reversible y gradual para cualquier actualización de biblioteca importante en un entorno de misión crítica.


