Una solicitud Todo, dos arquitecturas: qué elimina htmx de una pila CRUD
Sigue una única solicitud de agregar elemento a través de una SPA de React y una página htmx para ver qué capas desaparecen, qué asume el servidor y dónde deja de ser viable htmx.
Fuentes: Stop Over-Engineering CRUD Apps: Why HTMX Might Be All You Need
La mayoría de las herramientas internas y paneles de administración realizan las mismas cuatro acciones: crear, leer, actualizar y eliminar registros. Sin embargo, la forma habitual de desarrollarlos se ha convertido en una aplicación completa de una sola página, con un framework de componentes, un gestor de estado, una biblioteca para obtener datos, endpoints JSON y código del cliente cuya función principal es convertir ese JSON de nuevo en HTML. En algún momento, un formulario que inserta una fila en la base de datos acaba dependiendo de media docena de paquetes y dos herramientas de compilación.
Este artículo analiza una función concreta, la adición de un elemento a una lista de tareas, tanto mediante una implementación con React como con htmx. Al observar el ciclo de vida de la solicitud lado a lado, resulta sencillo determinar qué capas realmente necesita tu propia aplicación CRUD y cuándo es adecuado recurrir a un framework cliente más potente. Para una comparación más amplia entre hypermedia y el enfoque por defecto de las SPA, consulta htmx vs el enfoque por defecto de las SPA.
Los requisitos reales que tienen la mayoría de las aplicaciones CRUD
Reduzca una aplicación empresarial típica a lo que hacen sus usuarios: inician sesión, crean registros, los editan y los eliminan. Por lo general no hay modo sin conexión, ni colaboración en tiempo real que reconciliar, ni interacciones tan rápidas que requieran ser procesadas sin realizar una solicitud al servidor. Nada de lo mencionado justifica la necesidad de utilizar un DOM virtual, el enrutamiento en el cliente o esperar casi un minuto a que una herramienta reconstruya un solo botón.
A menudo se eligen los frameworks por costumbre, antes incluso de que alguien escriba qué debe hacer la aplicación. Partir de los requisitos suele llevar a un conjunto de herramientas mucho más reducido.
Cómo htmx cambia el contrato con el servidor
htmx permite que los elementos HTML comunes envíen solicitudes HTTP a través de atributos. El servidor responde con un fragmento HTML en lugar de JSON, y htmx inserta ese fragmento en la página en la ubicación que usted especifique.
La consecuencia es que no existe una copia del lado del cliente de los datos para gestionar. El servidor ya sabe cómo debe verse la lista, por lo que genera el marcado final y lo envía. El navegador solo tiene que colocarlo en el lugar correcto.
La misma funcionalidad en ambos estilos
La versión de React
Aquí hay un componente React convencional que agrega tareas pendientes. Almacena el texto de entrada y la lista en el estado, envía JSON a una API, analiza la respuesta, agrega el nuevo elemento y borra el campo.
function AddTodo() {
const [text, setText] = useState("")
const [todos, setTodos] = useState([])
const addTodo = async () => {
const res = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ text }),
})
const todo = await res.json()
setTodos([...todos, todo])
setText("")
}
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(t => <li key={t.id}>{t.text}</li>)}
</ul>
</div>
)
}
Incluso este pequeño ejemplo contiene detalles que deben ser correctos. La solicitud envía un cuerpo JSON sin el encabezado Content-Type, lo cual hará que muchos servidores no lo interpreten como JSON. La función de actualización setTodos([...todos, todo]) lee todos del renderizado en el que ocurrió el clic, por lo que dos envíos rápidos pueden hacer que se pierda un elemento; la forma funcional setTodos(prev => [...prev, todo]) evita esto. Además, la lista solo contiene elementos añadidos en esta sesión, ya que cargar los todos existentes requeriría otro efecto y endpoint adicional.
La versión htmx
El equivalente en htmx es un formulario y una lista vacía.
<form hx-post="/todos" hx-target="#list" hx-swap="beforeend">
<input name="text" />
<button>Add</button>
</form>
<ul id="list"></ul>
Tres atributos determinan el comportamiento. hx-post="/todos" envía los campos del formulario a esa URL cuando se presenta el formulario. hx-target="#list" indica a dónde debe ir la respuesta, y hx-swap="beforeend" lo agrega dentro de ese elemento, después de los hijos existentes, en lugar de reemplazar su contenido. El botón no necesita un manejador, ya que un botón dentro de un formulario lo presenta por defecto.
En el servidor, la ruta lee los campos del formulario, almacena la tarea y muestra una fila de la lista a partir de un template.
@app.route("/todos", methods=["POST"])
def add_todo():
text = request.form["text"]
todo = save_todo(text)
return render_template("todo_row.html", todo=todo)
Ese es todo el frontend para esta función: sin hooks, sin estado del cliente y sin comportamiento de volver a renderizar para depurar tarde en la noche. La plantilla todo_row.html simplemente renderiza un único <li> para el nuevo elemento. Dado que se puede utilizar la misma plantilla cuando se renderiza por primera vez la página, la lista inicial y las filas recién añadidas comparten un mismo fragmento de marcado en lugar de un componente JSX y una vista del servidor que deban mantenerse sincronizados.
Existen algunas notas prácticas aplicables a despliegues reales. Una solicitud que modifica el estado sigue necesitando protección CSRF, generalmente incluyendo el token como un campo oculto o en un encabezado de solicitud. Además, si también se asignan al formulario los atributos habituales action y method, seguirá funcionando como una simple solicitud de envío de formulario cuando JavaScript no se cargue correctamente.
Rastreando los dos ciclos de vida de la solicitud
La diferencia se hace más evidente al dibujar cada paso del flujo paso a paso. Primero, la ruta SPA.
Browser Server
-------- --------
Click button
|
v
JS event handler
|
v
fetch() call -------> API endpoint
|
v
Query database
|
v
Return JSON
^ |
|__________________________|
|
v
Parse JSON
|
v
Update state
|
v
Re-render component
|
v
Diff virtual DOM
|
v
Patch real DOM
Después de que la base de datos devuelve JSON, el cliente aún debe parsearlo, actualizar el estado, volver a renderizarlo, hacer una comparación y aplicar cambios al DOM.
Ahora, la ruta htmx para el mismo clic.
Browser Server
-------- --------
Click button
|
v
htmx request -------> Route handler
|
v
Query database
|
v
Render HTML piece
^ |
|__________________________|
|
v
Swap HTML into page
Varias etapas simplemente desaparecen: el parseo de JSON en el lado del cliente, las actualizaciones de estado, las comparaciones y cualquier esfuerzo por mantener una copia del cliente de los datos consistente. La base de datos detrás del servidor siempre ha sido dueña de estos datos. El enfoque htmx deja de mantener una segunda versión del estado de los datos en el lado del cliente.
Qué cambia en la práctica
En una migración documentada de un proyecto administrativo para clientes, reemplazar la interfaz frontal en React por plantillas generadas en el servidor junto con htmx redujo el paquete de JavaScript de aproximadamente 340 KB a unos 12 KB, mientras que el tiempo necesario para que la aplicación fuera interactiva en un teléfono de gama media pasó de casi tres segundos a menos de un segundo. Las solicitudes de integración disminuyeron porque había menos código por revisar, y los nuevos desarrolladores podían familiarizarse con el proyecto en una tarde, ya que la base de código estaba compuesta principalmente por HTML con unos pocos atributos adicionales.
Considere esas cifras como una ilustración, no como un punto de referencia. Proceden de un único proyecto medido de forma informal en una máquina y red específicas. La dirección del efecto es plausible para cualquier aplicación donde la mayor parte del código del cliente sirve únicamente para transferir datos entre JSON y el DOM, pero mida su propia aplicación antes y después en lugar de asumir los mismos beneficios.
Cuando htmx no es la herramienta adecuada
Esto no es un argumento a favor de eliminar React de todo. Las aplicaciones que requieren interacciones realmente complejas en el lado del cliente, como herramientas de diseño, hojas de cálculo o editores colaborativos en tiempo real, necesitan un estado significativo del cliente y un framework diseñado para gestionarlo. Forzarlas a utilizar interacciones de solicitud e intercambio sería un esfuerzo constante.
Una forma rápida de decidir es hacerse algunas preguntas sobre la función que se tiene ante uno:
- ¿La interacción necesita responder más rápido que un viaje de ida y vuelta por la red, como al arrastrar, dibujar o escribir con retroalimentación inmediata?
- ¿Debe funcionar sin conexión o reconciliar ediciones de varios usuarios en tiempo real?
- ¿El cliente necesita almacenar y transformar una gran cantidad de datos localmente?
Si las respuestas son en su mayoría “no”, la característica en cuestión probablemente se refiere a formularios, tablas y botones que guardan datos en una base de datos. Esa categoría abarca mucho más software del que es conveniente admitir, y para ello la infraestructura adicional en el lado del cliente tiende a aumentar los costos de mantenimiento en lugar de resolver un problema.
Conclusión
- Elija las herramientas según los requisitos; los sistemas CRUD simples rara vez necesitan estado en el lado del cliente.
- Con htmx, el servidor renderiza los fragmentos y los atributos determinan dónde se mostrarán, eliminando la necesidad de traducir datos en JSON y evitando una fuente de información duplicada.
- Mantenga la protección contra CSRF, los atributos
actionreales y las plantillas de fila compartidas. - Reserve un framework completo para el lado del cliente para interacciones que realmente requieran mucha lógica allí.
Aprender React sigue siendo una decisión profesional sensata, y muchos proyectos lo requieren. Eso justifica conocer la herramienta; no justifica incluirla en todas las aplicaciones cuyas necesidades reales son un formulario y una tabla.