Startseite / Artikel

Eine Todo-Anfrage, zwei Architekturen: Was htmx von einem CRUD-Stack weglässt

Verfolgen Sie eine einzelne Anfrage zum Hinzufügen eines Elements durch eine React SPA und eine htmx-Seite, um zu erkennen, welche Schichten verschwinden, was der Server übernimmt und wo htmx nicht mehr funktioniert.

Verlag: REACTAPP.TOP6 Min. Lesezeit1479 Wörter

Quellen: Stop Over-Engineering CRUD Apps: Why HTMX Might Be All You Need

Die meisten internen Tools und Admin-Panels erledigen dieselben vier Aufgaben: Das Erstellen, Lesen, Aktualisieren und Löschen von Datensätzen. Doch die übliche Vorgehensweise bei ihrer Entwicklung besteht darin, vollständige Ein-Seiten-Anwendungen zu schaffen, die über ein Komponentenframework, einen Zustandsmanager, eine Bibliothek zum Abrufen von Daten, JSON-Endpunkte sowie Client-Code verfügen, dessen Hauptaufgabe darin besteht, dieses JSON wieder in HTML umzuwandeln. Irgendwann hängt ein Formular, das einen Datensatz in der Datenbank einfügt, von einem Dutzend Paketen und zwei Build-Tools ab.

Dieser Artikel verfolgt eine konkrete Funktion – das Hinzufügen eines Eintrags zu einer To-Do-Liste – und zeigt dies sowohl in einer React- als auch in einer htmx-Implementierung. Das Vergleichen des Anfragenlebenszyklus Seite an Seite macht es einfach, zu beurteilen, welche Schichten Ihre eigene CRUD-App tatsächlich benötigt und wann ein komplexeres Client-Framework immer noch die richtige Wahl ist. Für einen umfassenderen Vergleich von Hypermedia im Vergleich zum Standard-SPA siehe htmx vs the SPA default.

Die tatsächlichen Anforderungen der meisten CRUD-Apps

Eine typische Geschäftsanwendung auf das Essentielle reduzieren – nämlich das, was ihre Nutzer tun. Sie melden sich an, erstellen Einträge, bearbeiten sie und löschen sie. In der Regel gibt es keinen Offline-Modus, keine Echtzeit-Kollaboration zur Abstimmung und keine so schnellen Interaktionen, dass sie ohne Rückfrage beim Server abgewickelt werden müssten. Nichts auf dieser Liste rechtfertigt die Verwendung eines virtuellen DOMs, Routing am Client oder das Warten von fast einer Minute darauf, dass eine Toolkette einen einzigen Button neu erstellt.

Frameworks werden oft aus Gewohnheit gewählt, noch bevor jemand aufschreibt, was die Anwendung tun muss. Mit der Ausgangsbasis in den Anforderungen erhält man in der Regel ein viel kleineres Werkzeugset.

Wie htmx den Vertrag mit dem Server verändert

htmx ermöglicht es gewöhnlichen HTML-Elementen, über Attribute HTTP-Anfragen zu senden. Der Server antwortet mit einem HTML-Fragment statt mit JSON, und htmx fügt dieses Fragment an die von Ihnen angegebene Stelle auf der Seite ein.

Die Folge ist, dass es keine Client-Seite-Kopie der Daten gibt, die verwaltet werden müsste. Der Server weiß bereits, wie die Liste aussehen sollte, daher rendernt er das fertige Markup und sendet es. Der Browser muss es nur an den richtigen Platz setzen.

Dieselbe Funktion in beiden Stilen

Die React-Version

Hier ist ein herkömmliches React-Komponent, das eine To-Do-Aufgabe hinzufügt. Es speichert den Eingabetext und die Liste im State, sendet JSON an eine API, parset die Antwort, fügt den neuen Eintrag hinzu und leert das Feld.

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>
  )
}

Auch dieses kleine Beispiel enthält Details, die korrekt umgesetzt werden müssen. Die Anfrage sendet einen JSON-Body ohne Content-Type-Header, wodurch viele Server den Inhalt nicht als JSON interpretieren. Die Aktualisierung setTodos([...todos, todo]) liest todos aus dem Render-Datenstrom ab, in dem der Klick stattfand, sodass zwei schnelle Abfragen ein Element verlieren können; die funktionale Form setTodos(prev => [...prev, todo]) vermeidet das. Zudem enthält die Liste nur Elemente, die in dieser Sitzung hinzugefügt wurden, da das Laden bereits vorhandener Elemente einen weiteren Effekt und eine weitere Endpunkt-URL erfordern würde.

Die htmx-Version

Der htmx-Äquivalent besteht aus einem Formular und einer leeren Liste.

<form hx-post="/todos" hx-target="#list" hx-swap="beforeend">
  <input name="text" />
  <button>Add</button>
</form>

<ul id="list"></ul>

Drei Attribute bestimmen das Verhalten. hx-post="/todos" sendet die Formfelder an diese URL, wenn das Formular abgesendet wird. hx-target="#list" gibt an, wohin die Antwort gesendet werden soll, und hx-swap="beforeend" fügt sie innerhalb dieses Elements, nach den vorhandenen Kindern, ein, anstatt dessen Inhalt zu ersetzen. Der Button benötigt keinen Handler, da ein Button innerhalb eines Formulars dieses standardmäßig absendet.

Auf der Serverseite liest die Route das Formularfeld, speichert den Todo und rendernt eine Zeile der Liste aus einem 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)

Das ist die gesamte Frontend-Struktur für diese Funktion: keine Hooks, kein Client-State und kein Neuladenverhalten zum Debuggen mitten in der Nacht. Das Template todo_row.html rendernt einfach ein einzelnes <li> für den neuen Eintrag. Da dasselbe Template auch beim ersten Renderen der Seite verwendet werden kann, teilen sich die ursprüngliche Liste sowie die neu hinzugefügten Zeilen denselben Markup-Teil, anstatt eine JSX-Komponente sowie eine Serveransicht zu verwenden, die synchron gehalten werden müssten.

Für echte Implementierungen gelten einige praktische Hinweise. Eine Anfrage, die den State ändert, benötigt weiterhin CSRF-Schutz – in der Regel durch Hinzufügen des Tokens als verstecktes Feld oder als Anfrage-Header. Wenn man dem Formular außerdem normale action- und method-Attribute gibt, funktioniert es weiterhin als einfaches Formular-Submit, falls JavaScript nicht geladen werden kann.

Aufspüren der beiden Anfrage-Lebenszyklen

Der Unterschied wird am deutlichsten, wenn man jeden Schritt des Ablaufs schrittweise nachzeichnet. Zuerst der SPA-Route.

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

Nachdem die Datenbank JSON zurückgegeben hat, muss der Client es dennoch parsen, den Zustand aktualisieren, neu rendern, Unterschiede ermitteln und den DOM anpassen.

Nun der htmx-Route für denselben Klick.

Browser                    Server
--------                   --------
Click button
   |
   v
htmx request  ------->  Route handler
                              |
                              v
                         Query database
                              |
                              v
                         Render HTML piece
   ^                          |
   |__________________________|
   |
   v
Swap HTML into page

Mehrere Schritte entfallen einfach: die Client-seitige JSON-Parse, Zustandsaktualisierungen, Unterschiedsermittlung sowie alle Bemühungen, eine konsistente Client-Kopie der Daten aufrechtzuerhalten. Die Datenbank hinter dem Server besaß diese Daten die ganze Zeit über. Der htmx-Ansatz beendet die Aufrechterhaltung einer zweiten, client-seitigen Version dieser Daten.

Was ändert sich in der Praxis

In einer berichteten Migration eines Client-Admin-Projekts verringerte das Ersetzen der React-Frontend mit serverseitig generierten Templates zusammen mit htmx das JavaScript-Bundle von etwa 340 KB auf rund 12 KB, während die Zeit bis zur Interaktivität auf einem Mittelklasse-Telefon von fast drei Sekunden auf unter eine Sekunde sank. Die Anzahl der Pull Requests nahm ab, da weniger Code geprüft werden musste, und neue Entwickler konnten sich bereits am Nachmittag zurechtfinden, da die Codebasis hauptsächlich aus HTML mit einigen zusätzlichen Attributen bestand.

Betrachten Sie diese Zahlen als Veranschaulichung, nicht als Vergleichswert. Sie stammen von einem einzelnen Projekt, das informell auf einer Maschine und im jeweiligen Netzwerk gemessen wurde. Die Richtung des Effekts ist für jede Anwendung plausibel, in der der größte Teil des Client-Codes lediglich dazu dient, Daten zwischen JSON und dem DOM zu übertragen – messen Sie jedoch Ihre eigene Anwendung vor und nach der Umsetzung, anstatt von denselben Vorteilen auszugehen.

Wann htmx das falsche Werkzeug ist

Dies ist kein Argument dafür, React aus allem zu entfernen. Anwendungen mit wirklich umfangreichen Interaktionen auf der Client-Seite, wie z. B. Design-Tools, Tabellenkalkulationen oder Echtzeit-Kollaborationseditoren, benötigen einen umfangreichen Client-Zustand sowie ein Framework, das dazu in der Lage ist, diesen zu verwalten. Solche Anwendungen mit solchen Anforderungen auf Requests und -Responses zu beschränken, würde ständige Herausforderungen mit sich bringen.

Eine schnelle Möglichkeit, eine Entscheidung zu treffen, besteht darin, sich einige Fragen zur vorliegenden Funktion zu stellen:

  • Muss die Interaktion schneller reagieren als eine Netzwerkverbindung hergestellt werden kann, beispielsweise bei Drag-and-Drop, Zeichnen oder Tippen mit sofortiger Rückmeldung?
  • Muss die Anwendung offline funktionieren oder Änderungen mehrerer Benutzer in Echtzeit abgleichen?
  • Muss der Client große Mengen an Daten lokal speichern und verarbeiten?

Falls die Antworten größtenteils „Nein“ lauten, handelt es sich wahrscheinlich um Formulare, Tabellen und Schaltflächen, die Daten in einer Datenbank speichern. Diese Kategorie umfasst weitaus mehr Software, als es üblich ist zuzugeben, und die zusätzlichen Client-Komponenten führen eher zu höheren Wartungskosten als dass sie Probleme lösen.

Zusammenfassung

  • Wählen Sie Werkzeuge entsprechend den Anforderungen aus; einfache CRUD-Anwendungen benötigen selten einen Client-Side-Zustand.
  • Mit htmx rendernt der Server Fragmente und Attribute bestimmen, wo sie angezeigt werden, wodurch eine JSON-Übersetzung sowie eine doppelte Quelle der Wahrheit entfallen.
  • Bewahren Sie die CSRF-Schutzmaßnahmen, echte action-Attribute sowie gemeinsame Zeilenvorlagen bei.
  • Reservieren Sie ein vollständiges Client-Framework für wirklich clientintensive Interaktionen.

Das Erlernen von React bleibt eine sinnvolle Karrierewahl, und viele Projekte benötigen es. Das rechtfertigt zwar das Kennen des Tools, aber nicht seine Verwendung in jeder Anwendung, deren tatsächliche Bedürfnisse lediglich ein Formular und eine Tabelle sind.