Saltar a contenido

ADR-0006 · Next.js 16 con React 19 estable, npm como único gestor y updateTag para invalidar caché

Este ADR se numeró primero como 0004 en la rama mvaliente-desa. Al integrarse con develop, que ya tenía un ADR-0004 y un ADR-0005 distintos, pasó a ser el 0006.

  • Estado: aceptado
  • Decisores: mvaliente1
  • Fecha de la decisión: 2026-08-31

Contexto y problema

El storefront corría sobre Next 15 y una release candidate de React (19.0.0-rc-66855b96-20241106, de noviembre de 2024), fijada a mano y sostenida con resolutions (types-react@19.0.0-rc.1) y overrides. Un RC clavado a una fecha no recibe parches: cualquier corrección de React quedaba fuera de alcance sin tocar ese andamiaje.

Al mismo tiempo, el repositorio arrastraba dos lockfiles. El ADR-0003 lo aceptó como deuda y ya anotaba la consecuencia: "una dependencia agregada con yarn add no llega a la imagen hasta que se actualiza package-lock.json: lo que se prueba en local no es necesariamente lo que se despliega". En los hechos la deuda ya se había resuelto sola en una dirección: el node_modules del entorno de desarrollo estaba instalado con npm, no con Yarn, y yarn ni siquiera estaba en el PATH.

Opciones consideradas

  1. Subir sólo React a estable y quedarse en Next 15.
  2. Subir Next a 16 manteniendo el RC de React.
  3. Migrar ambos y unificar el gestor de paquetes en npm (elegida).

La opción 2 no es viable: no tiene sentido adoptar un major nuevo sobre una dependencia sin soporte. La 1 posterga el problema sin resolverlo.

Decisión

Se migró a Next 16.3.3 + React 19.2.8 estable, en dos etapas verificadas por separado (primero React sobre Next 15, después Next 16), y se unificó el repositorio en npm: se eliminó yarn.lock y la clave packageManager.

Los cambios que la migración obligó, más allá de subir versiones:

  • revalidateTag → updateTag (35 llamadas en src/lib/data/cart.ts y src/lib/data/customer.ts). En Next 16 revalidateTag(tag) sin segundo argumento quedó deprecado y su semántica cambió: ahora sirve stale content mientras revalida en segundo plano. La purga inmediata con lectura de los propios cambios pasó a llamarse updateTag, y sólo puede invocarse dentro de una Server Action. Se verificó que las 35 llamadas cumplen esa condición.
  • Turbopack como bundler de producción. Es el default de Next 16. El proyecto no tenía configuración propia de webpack ni de Babel, así que se eliminaron las dependencias webpack, babel-loader y @babel/core, que no se usaban. Se fijó turbopack.root en next.config.js para que Turbopack no infiera la raíz del proyecto desde un package-lock.json de un directorio superior.
  • ESLint 9 con flat config. Next 16 eliminó el comando next lint y dejó de aceptar la clave eslint en next.config.js. .eslintrc.js se reemplazó por eslint.config.mjs y el script lint pasó a ser eslint ..
  • params asíncronos. Cuatro rutas todavía tipaban params como objeto plano; en Next 16 es una Promise.
  • tsconfig.json: Next 16 exige moduleResolution: "bundler" y jsx: "react-jsx", y los reescribe solo al construir.
  • Se eliminó src/app/auth/google/callback/page.tsx, código muerto que importaba un símbolo inexistente (handleGoogleCallback). Webpack lo dejaba pasar; Turbopack falla el build. El flujo real de OAuth vive en src/app/[countryCode]/auth/google/callback/page.tsx.

El bloque overrides se mantiene: @medusajs/ui sigue declarando peer react ^18.3.1 y sin el override npm degrada React a 18.

Consecuencias

Positivas

  • React y Next en versiones con soporte, parcheables por rango semver.
  • Un solo lockfile: se cierra la deuda principal que el ADR-0003 dejó anotada. Lo que se prueba en local es lo que se construye en la imagen.
  • Builds notablemente más rápidos con Turbopack (~10 s de compilación).
  • Menos andamiaje: sin resolutions, sin webpack/babel-loader/@babel/core.
  • El cambio a updateTag hace explícita la semántica que el carrito necesitaba desde siempre: leer los propios cambios sin servir contenido viejo.

Negativas / deuda asumida

  • middleware.ts migrado a proxy.ts (hecho el 2026-09-03). El codemod oficial no corre con el árbol de trabajo sucio, así que se hizo a mano: git mv, export async function middleware → proxy, y dos mensajes de error internos. Verificado: ƒ Proxy (Middleware) en la tabla de rutas, sin warning de deprecación.
  • ESLint 9 activa reglas nuevas (react-hooks v6) que antes no existían: 26 errores y 20 warnings preexistentes quedaron al descubierto, la mayoría react-hooks/exhaustive-deps y @next/next/no-img-element. Ninguno bloquea el build, pero npm run lint no está en verde.
  • Node 20.18.3 se queda corto para el toolchain de ESLint 9, que pide ^20.19.0 || ^22.13.0 || >=24 (npm warn EBADENGINE por eslint-visitor-keys). Next 16 sí funciona (pide >=20.9.0). El destino es Node 24 "Krypton", no 22: Node 20 ya está EOL, 22 está en mantenimiento y muere en abril 2027, y 24 es el Active LTS hasta abril 2028. Verificado el 2026-09-03 en v24.20.0, sobre los dos repos: npm ci y npm run build en verde, y ambos servidores levantados y respondiendo. Con Node 24 el EBADENGINE desaparece. Falta que el encargado de infra suba el node:20-alpine de los Dockerfile; package.json ya declara "engines": { "node": ">=24" } y hay .nvmrc en ambos repos. Nota: Node 24 trae npm 11.19, que por defecto ya no ejecuta los scripts de instalación de las dependencias. No rompe el build.
  • El ADR-0003 sigue vigente en lo que respecta a cómo se construye la imagen; sólo queda sin efecto su premisa de que el repositorio versiona dos lockfiles.

Verificación

npm run build en verde (685 páginas estáticas generadas) y prueba manual del dev server sobre /py, /py/home, /py/store, /py/cart, /py/account, /py/categories/*, /py/collections/* y fichas de producto: todas 200. tsc --noEmit pasó de 22 a 21 errores, todos preexistentes al cambio: la migración no introdujo ninguno. Esos 21 se corrigieron el 2026-09-21 (tarea

272) y con eso se quitó typescript.ignoreBuildErrors del next.config.js:

el build vuelve a fallar ante un error de tipos.

Validado a mano sobre Node 24, por ser el punto de mayor riesgo del cambio a updateTag:

Server action Estado
addToCart verificada (2026-09-02 y 2026-09-03)
setShippingMethod, initiatePaymentSession, placeOrder verificadas: orden #120 con transferencia bancaria, redirección a /confirmed y carrito vaciado
deleteLineItem verificada (2026-09-03)
login, signout verificadas (2026-09-03)
updateLineItem (cambiar cantidad) verificada (2026-09-03, cantidad 1 → 4)
applyPromotions (aplicar promoción) pendiente
updateRegion (cambiar de región) pendiente

Al margen, ESLint 9 destapó dos defectos preexistentes que se corrigieron en el mismo ciclo: en CreditButton un useCallback quedaba después de un return temprano, con lo que la cantidad de hooks dependía del precio y React rompía la ficha de producto al cambiar de variante cruzando el mínimo de crédito; y en cart-dropdown un itemRef.current en el array de dependencias, que no dispara re-render y volvía inerte esa dependencia.