Migración desde htmx 2
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:
| Atributo | htmx 2 | htmx 4 |
|---|---|---|
hx-disable | No procesar htmx en el subárbol | Deshabilitar elementos durante la petición |
hx-disabled-elt | Deshabilitar 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
sedsobre un árbol de trabajo limpio y revisa el diff antes de commitear.
Tabla completa de cambios
Atributos eliminados
| Eliminado | Reemplazo |
|---|---|
hx-vars | hx-vals con prefijo js: |
hx-params | Evento htmx:config:request + ctx.request.body.delete() |
hx-prompt | Extensión hx-prompt, o hx-on con una línea |
hx-ext | Incluir el script de la extensión directamente |
hx-disinherit | Innecesario: la herencia es explícita |
hx-inherit | Innecesario: usar :inherited |
hx-disabled-elt | hx-disable |
Atributos renombrados
| htmx 2 | htmx 4 |
|---|---|
hx-disable | hx-ignore |
hx-disabled-elt | hx-disable |
Atributos nuevos
| Atributo | Qué hace |
|---|---|
hx-status:<código> | Comportamiento por código HTTP |
hx-config | Configuración por elemento o subárbol |
hx-action / hx-method | URL y verbo por separado |
hx-optimistic | UI optimista (extensión) |
hx-preload | Precarga al hover (extensión) |
hx-live | Bindings reactivos (extensión) |
<hx-partial> | Elemento de fragmento con destino propio |
Modificadores nuevos
| Modificador | Qué hace |
|---|---|
:inherited | El atributo se hereda a los descendientes |
:append | El valor local se fusiona con el heredado |
Eventos renombrados
| htmx 2 | htmx 4 |
|---|---|
htmx:beforeRequest | htmx:before:request |
htmx:afterRequest | htmx:after:request |
htmx:beforeSwap | htmx:before:swap |
htmx:afterSwap | htmx:after:swap |
htmx:beforeSettle | htmx:before:settle |
htmx:afterSettle | htmx:after:settle |
htmx:afterOnLoad | htmx:after:init |
htmx:configRequest | htmx:config:request |
htmx:responseError | htmx:response:error |
htmx:beforeCleanupElement | htmx:before:cleanup |
Eventos eliminados
| Eliminado | Motivo |
|---|---|
htmx:xhr:* | Ya no se usa XMLHttpRequest |
htmx:validation:* | Se usa la validación HTML5 nativa |
Configuración renombrada
| htmx 2 | htmx 4 | Nota |
|---|---|---|
defaultSwapStyle | defaultSwap | |
historyEnabled | history | Ahora acepta true/false/"reload" |
timeout | defaultTimeout | El default pasó de 0 a 60000 |
selfRequestsOnly | mode: 'same-origin' | |
allowEval | (eliminado) | |
allowScriptTags | (eliminado) |
API JavaScript eliminada
| Eliminado | Reemplazo |
|---|---|
htmx.addClass/removeClass/toggleClass | element.classList.* |
htmx.closest | element.closest() |
htmx.remove | element.remove() |
htmx.off | removeEventListener() |
htmx.values | new 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-checky guardar el reporte - Desplegar htmx 4 con
htmx-2-compaty verificar en staging - Renombrar
hx-disable→hx-ignore(primero) - Renombrar
hx-disabled-elt→hx-disable(después) - Sustituir
hx-varsporhx-valsconjs: - Sustituir
hx-paramsporhtmx:config:request - Quitar
hx-exte incluir los scripts de las extensiones - Quitar
hx-disinherit/hx-inherit - Renombrar los eventos en todo el JavaScript
- Sustituir
htmx.addClassy compañía por API nativa del DOM - Añadir
:inheriteddonde corresponda y desactivarimplicitInheritance - Borrar los listeners de
beforeSwapque forzaban el swap de errores - Revisar operaciones largas contra el timeout de 60 s
- Añadir
hx-include="closest form"a loshx-deleteque 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-checkaudita el proyecto;htmx-2-compates la red de seguridad.- El renombre
hx-disable→hx-ignoredebe hacerse antes quehx-disabled-elt→hx-disable. - Los cambios más peligrosos son los de comportamiento: herencia, swap de errores, timeout,
hx-deletee historial. htmx:after:implicitInheritancete dice exactamente dónde falta un:inherited.- Migrar por rutas permite avanzar sin un despliegue de todo o nada.
Siguiente: Seguridad y producción →.