Une demande de tâche, deux architectures : ce que htmx retire d’une pile CRUD
Suivez une seule demande d’ajout d’élément à travers un SPA React et une page htmx pour voir quels niveaux disparaissent, ce que le serveur prend en charge, et où htmx cesse de fonctionner.
Sources: Stop Over-Engineering CRUD Apps: Why HTMX Might Be All You Need
La plupart des outils internes et des panneaux d’administration effectuent les mêmes quatre actions : créer, lire, mettre à jour et supprimer des enregistrements. Pourtant, la méthode par défaut pour les développer consiste à créer une application unique sur une seule page, dotée d’un framework de composants, d’un gestionnaire d’état, d’une bibliothèque de récupération de données, d’interfaces JSON et de code client dont la fonction principale est de convertir ce JSON en HTML. Au fil du processus, un formulaire permettant d’insérer une ligne dans la base de données finit par dépendre de plusieurs dizaines de packages et de deux outils de compilation.
Cet article suit une fonctionnalité concrète, à savoir l’ajout d’un élément à une liste de tâches, en présentant à la fois une implémentation avec React et une implémentation avec htmx. En comparant côte à côte le cycle de vie des requêtes, il devient facile de déterminer quels composants votre propre application CRUD a réellement besoin, et quand un framework client plus lourd reste la meilleure solution. Pour une comparaison plus approfondie entre hypermedia et le modèle par défaut des SPA, consultez htmx vs le SPA par défaut.
Les exigences réelles des applications CRUD
Décomposez une application professionnelle typique en ce que font ses utilisateurs : ils se connectent, créent des enregistrements, les modifient et les suppriment. Il n’y a généralement pas de mode hors ligne, aucune collaboration en temps réel à synchroniser, et aucune interaction si rapide qu’elle doive être traitée sans aller chercher des informations sur le serveur. Rien dans cette liste ne justifie l’utilisation d’un DOM virtuel, du routage côté client, ou l’attente d’une minute pour que des outils reconstruisent un simple bouton.
Les frameworks sont souvent choisis par habitude, avant même que quiconque n’ait noté ce que l’application doit faire. Partir des exigences conduit généralement à un ensemble d’outils bien plus restreint.
Comment htmx change le contrat avec le serveur
htmx permet aux éléments HTML ordinaires d’envoyer des requêtes HTTP via des attributs. Le serveur répond par un fragment HTML plutôt que par du JSON, et htmx insère ce fragment dans la page à l’emplacement que vous spécifiez.
La conséquence est qu’il n’y a pas de copie côté client des données à gérer. Le serveur sait déjà à quoi doit ressembler la liste, il génère donc le markup final et l’envoie. Le navigateur n’a qu’à l’insérer à l’endroit approprié.
La même fonctionnalité dans les deux styles
La version React
Voici un composant React conventionnel qui ajoute une tâche à faire. Il conserve le texte saisi et la liste dans l’état, envoie du JSON vers une API, analyse la réponse, ajoute le nouvel élément et vide le champ.
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>
)
}
Même cet exemple simple comporte des détails qu’il faut impérativement respecter. La requête envoie un corps JSON sans en-tête Content-Type, ce que de nombreux serveurs ne parviendront pas à interpréter comme JSON. La fonction d’actualisation setTodos([...todos, todo]) lit la liste todos à partir de l’élément affiché au moment du clic, ce qui peut faire perdre un élément en cas de deux soumissions rapides ; la version fonctionnelle setTodos(prev => [...prev, todo]) évite ce problème. De plus, la liste ne contient que les éléments ajoutés au cours de cette session, car charger les éléments existants nécessiterait un autre effet et une autre adresse URL.
La version htmx
L’équivalent en htmx consiste en un formulaire et une liste vide.
<form hx-post="/todos" hx-target="#list" hx-swap="beforeend">
<input name="text" />
<button>Add</button>
</form>
<ul id="list"></ul>
Trois attributs déterminent le comportement. hx-post="/todos" envoie les champs du formulaire vers cette URL lors de sa soumission. hx-target="#list" indique où doit aller la réponse, et hx-swap="beforeend" l’ajoute à l’intérieur de cet élément, après les enfants existants, au lieu de remplacer son contenu. Le bouton n’a pas besoin de gestionnaire, car un bouton à l’intérieur d’un formulaire le soumet par défaut.
Sur le serveur, la route lit les champs du formulaire, enregistre la tâche à effectuer et affiche une ligne de liste provenant d’un modèle.
@app.route("/todos", methods=["POST"])
def add_todo():
text = request.form["text"]
todo = save_todo(text)
return render_template("todo_row.html", todo=todo)
C’est là l’ensemble de l’interface utilisateur pour cette fonctionnalité : pas de hooks, pas d’état côté client et aucun comportement de réaffichage pour déboguer tard dans la nuit. Le modèle todo_row.html se contente d’afficher un seul élément <li> pour le nouvel élément. Comme le même modèle peut être utilisé lors du premier affichage de la page, la liste initiale et les lignes nouvellement ajoutées partagent un même morceau de markup plutôt qu’un composant JSX et une vue serveur qui devraient être synchronisés.
Quelques remarques pratiques s’appliquent aux déploiements réels. Une requête modifiant l’état nécessite toujours une protection CSRF, généralement en incluant le jeton dans un champ caché ou dans l’en-tête de la requête. De plus, si vous attribuez également aux formulaires les attributs ordinaires action et method, ils continueront à fonctionner comme des soumissions de formulaire classiques lorsque le JavaScript échoue à se charger.
Suivi des deux cycles de vie des requêtes
La différence est la plus évidente lorsque l’on trace chaque étape du flux pas à pas. D’abord, le chemin 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
Une fois que la base de données renvoie du JSON, le client doit encore le parser, mettre à jour l’état, réafficher l’interface, effectuer une comparaison et appliquer les modifications au DOM.
Puis, le chemin htmx pour ce même clic.
Browser Server
-------- --------
Click button
|
v
htmx request -------> Route handler
|
v
Query database
|
v
Render HTML piece
^ |
|__________________________|
|
v
Swap HTML into page
Plusieurs étapes disparaissent simplement : le parsing JSON côté client, les mises à jour d’état, les comparaisons, ainsi que tout effort visant à maintenir une copie client cohérente des données. La base de données hébergée sur le serveur possédait toujours ces données. L’approche htmx cesse de gérer une deuxième version côté client de cette information.
Quelles sont les changements en pratique
Lors d’une migration de projet administratif client rapportée, le remplacement du frontend React par des templates générés côté serveur ainsi que htmx a réduit le bundle JavaScript de près de 340 KB à environ 12 KB, tandis que le temps nécessaire pour obtenir une interaction sur un téléphone de gamme intermédiaire est passé de près de trois secondes à moins d’une seconde. Les demandes de fusion ont diminué car il y avait moins de code à examiner, et les nouveaux développeurs pouvaient maîtriser le projet en une après-midi, étant donné que la base de code se composait principalement d’HTML avec quelques attributs supplémentaires.
Considérez ces chiffres comme une illustration et non comme un critère de référence. Ils proviennent d’un seul projet évalué de manière informelle sur une machine et un réseau donnés. La direction de l’effet est plausible pour toute application où la majeure partie du code client sert uniquement à transférer des données entre JSON et le DOM, mais mesurez votre propre application avant et après plutôt que d’assumer les mêmes gains.
Lorsque htmx n’est pas l’outil adapté
Cela ne constitue pas une raison de retirer React de tous les projets. Les applications nécessitant des interactions côté client complexes, comme les outils de conception, les tableurs ou les éditeurs collaboratifs en temps réel, exigent un état client important ainsi qu’un framework conçu pour le gérer. Forcer ces applications à utiliser des interactions basées sur des requêtes et des échanges serait une lutte constante.
Une façon rapide de se décider consiste à se poser quelques questions au sujet de la fonctionnalité en question :
- L’interaction doit-elle répondre plus rapidement qu’un aller-retour réseau, par exemple pour le glisser-déposer, le dessin ou la saisie avec un retour immédiat ?
- Faut-il qu’elle fonctionne hors ligne, ou qu’elle concilie les modifications de plusieurs utilisateurs en temps réel ?
- Le client doit-il stocker et transformer une grande quantité de données localement ?
Si la plupart des réponses sont négatives, il s’agit probablement de formulaires, de tableaux et de boutons qui enregistrent des données dans une base de données. Cette catégorie englobe bien plus de logiciels que l’on n’a tendance à l’admettre, et les outils supplémentaires côté client ont plutôt pour effet d’augmenter les coûts de maintenance que de résoudre un problème.
Conclusion
- Choisissez les outils en fonction des besoins ; les systèmes CRUD simples n’ont généralement pas besoin d’un état côté client.
- Avec htmx, le serveur génère les fragments et les attributs déterminent leur emplacement, ce qui élimine la nécessité de traduire en JSON et supprime une source de vérité redondante.
- Préservez la protection CSRF, des attributs
actionréels ainsi que des modèles de lignes partagés. - Réservez un framework client complet pour les interactions véritablement lourdes en termes de traitement côté client.
Apprendre React reste un choix professionnel judicieux, et de nombreux projets en ont besoin. Cela justifie la maîtrise de cet outil ; cela ne justifie cependant pas son utilisation dans toutes les applications dont les besoins réels se limitent à un formulaire et une table.