Migración desde htmx 2

Por: Artiko
htmxhtmx4migracionhtmx2breaking-changes

Migración desde htmx 2

htmx 2 tiene soporte perpetuo declarado por el equipo de htmx: migrar es una decisión, no una urgencia. Este capítulo cubre todo lo que cambia y en qué orden hacerlo.

Estrategia

flowchart TD
    A[1. Auditar con upgrade-check] --> B[2. Cargar htmx 4 con htmx-2-compat]
    B --> C[3. Verificar que todo sigue funcionando]
    C --> D[4. Migrar por áreas: renombres primero]
    D --> E[5. Migrar la herencia a :inherited]
    E --> F[6. Quitar htmx-2-compat]
    F --> G[7. Adoptar lo nuevo: partials, morph, hx-status]

La clave es el paso 2: htmx 4 con la extensión de compatibilidad se comporta casi como htmx 2, así que puedes desplegar la nueva versión sin reescribir nada y migrar por partes.

Paso 1: auditar

htmx trae una herramienta que escanea tu proyecto y reporta lo que hay que cambiar:

npx [email protected] upgrade-check -- ./ruta/al/proyecto

Con extensiones de archivo adicionales:

npx [email protected] upgrade-check --ext .vue ./ruta/al/proyecto

Escanea .html, .php, .js, .ts, .jinja, .erb y otros formatos de plantilla. Requiere Python 3 instalado.

Paso 2: la red de seguridad

Dos formas de activar el modo compatible.

Por configuración:

<script>
  htmx.config.implicitInheritance = true;
  htmx.config.noSwap = [204, 304, '4xx', '5xx'];
</script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>

Por extensión, que además restaura los nombres de eventos antiguos:

<script src="/js/htmx.min.js"></script>
<script src="/js/ext/htmx-2-compat.js"></script>

Los renombres peligrosos

hx-disable cambió de significado

Este es el cambio que más silenciosamente rompe cosas, porque el atributo existe en ambas versiones con significados distintos:

Atributohtmx 2htmx 4
hx-disableNo procesar htmx en el subárbolDeshabilitar elementos durante la petición
hx-disabled-eltDeshabilitar elementos durante la petición(no existe)
hx-ignore(no existe)No procesar htmx en el subárbol

El orden de la migración importa. Hazlo en dos pasadas, en este orden:

# 1º: hx-disable → hx-ignore
grep -rl 'hx-disable=' src/ | xargs sed -i 's/hx-disable=/hx-ignore=/g'

# 2º: hx-disabled-elt → hx-disable
grep -rl 'hx-disabled-elt=' src/ | xargs sed -i 's/hx-disabled-elt=/hx-disable=/g'

Si lo haces al revés, conviertes los hx-disabled-elt en hx-disable y luego los renombras a hx-ignore, dejando la aplicación con subárboles enteros sin procesar y sin ningún error visible.

Ejecuta ese sed sobre un árbol de trabajo limpio y revisa el diff antes de commitear.

Tabla completa de cambios

Atributos eliminados

EliminadoReemplazo
hx-varshx-vals con prefijo js:
hx-paramsEvento htmx:config:request + ctx.request.body.delete()
hx-promptExtensión hx-prompt, o hx-on con una línea
hx-extIncluir el script de la extensión directamente
hx-disinheritInnecesario: la herencia es explícita
hx-inheritInnecesario: usar :inherited
hx-disabled-elthx-disable

Atributos renombrados

htmx 2htmx 4
hx-disablehx-ignore
hx-disabled-elthx-disable

Atributos nuevos

AtributoQué hace
hx-status:<código>Comportamiento por código HTTP
hx-configConfiguración por elemento o subárbol
hx-action / hx-methodURL y verbo por separado
hx-optimisticUI optimista (extensión)
hx-preloadPrecarga al hover (extensión)
hx-liveBindings reactivos (extensión)
<hx-partial>Elemento de fragmento con destino propio

Modificadores nuevos

ModificadorQué hace
:inheritedEl atributo se hereda a los descendientes
:appendEl valor local se fusiona con el heredado

Eventos renombrados

htmx 2htmx 4
htmx:beforeRequesthtmx:before:request
htmx:afterRequesthtmx:after:request
htmx:beforeSwaphtmx:before:swap
htmx:afterSwaphtmx:after:swap
htmx:beforeSettlehtmx:before:settle
htmx:afterSettlehtmx:after:settle
htmx:afterOnLoadhtmx:after:init
htmx:configRequesthtmx:config:request
htmx:responseErrorhtmx:response:error
htmx:beforeCleanupElementhtmx:before:cleanup

Eventos eliminados

EliminadoMotivo
htmx:xhr:*Ya no se usa XMLHttpRequest
htmx:validation:*Se usa la validación HTML5 nativa

Configuración renombrada

htmx 2htmx 4Nota
defaultSwapStyledefaultSwap
historyEnabledhistoryAhora acepta true/false/"reload"
timeoutdefaultTimeoutEl default pasó de 0 a 60000
selfRequestsOnlymode: 'same-origin'
allowEval(eliminado)
allowScriptTags(eliminado)

API JavaScript eliminada

EliminadoReemplazo
htmx.addClass/removeClass/toggleClasselement.classList.*
htmx.closestelement.closest()
htmx.removeelement.remove()
htmx.offremoveEventListener()
htmx.valuesnew FormData(form)

Cambios de comportamiento (sin renombre)

Estos son los que no detecta un grep: el código sigue siendo válido pero hace algo distinto.

1. Herencia de atributos

<!-- htmx 2: los botones apuntan a #resultado -->
<div hx-target="#resultado">
  <button hx-get="/a">A</button>
</div>

<!-- htmx 4: hay que pedirlo -->
<div hx-target:inherited="#resultado">
  <button hx-get="/a">A</button>
</div>

Sin :inherited, el botón apunta a sí mismo. No hay error en consola: simplemente el contenido aparece donde no debe.

Para auditar durante la migración, con implicitInheritance activado, escucha el evento que avisa cada vez que se aplicó herencia implícita:

htmx.on("htmx:after:implicitInheritance", (e) => {
  console.warn("Herencia implícita en:", e.target, e.detail);
});

Cada aviso es un sitio donde falta un :inherited.

2. Los errores hacen swap

// htmx 2: esto era necesario para ver un 422
htmx.on("htmx:beforeSwap", (e) => {
  if (e.detail.xhr.status === 422) e.detail.shouldSwap = true;
});

En htmx 4 ese listener sobra: bórralo. Y revisa el efecto inverso — si tu servidor devuelve páginas de error HTML del framework con código 500, ahora se van a insertar. Dos soluciones:

htmx.config.noSwap = [204, 304, '5xx'];

o mejor, devolver un fragmento propio para peticiones con HX-Request.

3. El timeout por defecto

De 0 (infinito) a 60000 ms. Si tienes operaciones largas (informes, subidas grandes), se van a cortar:

<button hx-post="/informe-anual" hx-config="timeout:600000">Generar</button>

4. hx-delete y los datos del formulario

<!-- htmx 2: incluía los campos del form automáticamente -->
<form>
  <input name="motivo">
  <button hx-delete="/items/1">Eliminar</button>
</form>

<!-- htmx 4: hay que pedirlo -->
<button hx-delete="/items/1" hx-include="closest form">Eliminar</button>

5. El historial ya no se cachea

Al pulsar “atrás”, htmx 4 vuelve a pedir el contenido. Requisito: tu servidor debe poder servir cada URL de forma completa. Si tus endpoints sólo devuelven fragmentos, el historial se rompe.

Solución rápida durante la migración:

<script src="/js/ext/hx-history-cache.js"></script>

Solución correcta: implementar “una ruta, dos respuestas” con el header HX-Request.

6. Idiomorph ya no es una extensión

Si usabas hx-ext="morph" con hx-swap="morph:innerHTML", ahora es nativo:

<!-- htmx 2 con la extensión -->
<div hx-ext="morph" hx-swap="morph:innerHTML">

<!-- htmx 4 -->
<div hx-swap="innerMorph">

Checklist de migración

  • Correr upgrade-check y guardar el reporte
  • Desplegar htmx 4 con htmx-2-compat y verificar en staging
  • Renombrar hx-disablehx-ignore (primero)
  • Renombrar hx-disabled-elthx-disable (después)
  • Sustituir hx-vars por hx-vals con js:
  • Sustituir hx-params por htmx:config:request
  • Quitar hx-ext e incluir los scripts de las extensiones
  • Quitar hx-disinherit / hx-inherit
  • Renombrar los eventos en todo el JavaScript
  • Sustituir htmx.addClass y compañía por API nativa del DOM
  • Añadir :inherited donde corresponda y desactivar implicitInheritance
  • Borrar los listeners de beforeSwap que forzaban el swap de errores
  • Revisar operaciones largas contra el timeout de 60 s
  • Añadir hx-include="closest form" a los hx-delete que lo necesiten
  • Verificar el botón “atrás” en todas las rutas
  • Quitar htmx-2-compat
  • Adoptar lo nuevo: <hx-partial>, innerMorph, hx-status

Migración incremental por rutas

En una aplicación grande, puedes servir versiones distintas por sección:

function scriptHtmx(ruta) {
  return rutasMigradas.some((r) => ruta.startsWith(r))
    ? `<script src="/js/htmx4.min.js"></script>`
    : `<script src="/js/htmx2.min.js"></script>`;
}

Feo pero efectivo: migras una sección, la verificas en producción y pasas a la siguiente. Como cada página carga una sola versión, no hay conflicto posible.

Resumen

  • htmx 2 tiene soporte perpetuo: migra cuando te convenga, no por presión.
  • upgrade-check audita el proyecto; htmx-2-compat es la red de seguridad.
  • El renombre hx-disablehx-ignore debe hacerse antes que hx-disabled-elthx-disable.
  • Los cambios más peligrosos son los de comportamiento: herencia, swap de errores, timeout, hx-delete e historial.
  • htmx:after:implicitInheritance te dice exactamente dónde falta un :inherited.
  • Migrar por rutas permite avanzar sin un despliegue de todo o nada.

Siguiente: Seguridad y producción →.