Die Frequenz neuer React-Releases ist weiterhin sehr gering: Das bislang letzte Minor-Update 18.2 erschien im Juni 2022 (Stand: Oktober 2023). Und dennoch tut sich sehr viel im Umfeld von React: Neben dem Fokus auf Fullstack-Frameworks und den damit verbundenen Änderungen gibt es auch Neuerungen in populären React-Bibliotheken, die Einfluss auf die Entwicklung von Single-Page-Anwendungen (SPA) haben.

Völlig unabhängig von einem neuen Release wurde im März 2023 die neue React-Dokumentation veröffentlicht, die zuvor schon in einer längeren Beta-Phase zugänglich war. Gegenüber der Beta-Version befinden sich darin allerdings einige Aussagen, die auf eine gewisse Neuausrichtung von React hindeuten. Für die Entwicklung von React-Anwendungen solle künftig ein „Fullstack-Framework“ wie Next.js oder Remix verwendet werden.

Damit scheint React eine Abkehr von der klassischen Single-Page-Anwendung zu vollziehen. Denn auch wenn sich mit Next.js theoretisch rein clientseitig gerenderte SPAs entwickeln lassen (über das „static exports“ Feature) liegt auch bei Next.js der Fokus klar auf der Entwicklung von React-Anwendungen, die zumindest teilweise auch auf dem Server gerendert werden.

Kontroverse Diskussion um Fullstack-Frameworks

Die Empfehlung für ein Fullstack-Framework begründet das React-Team mit einer Handvoll Probleme, die es bei der Entwicklung von SPAs ausgemacht haben will. Demnach würde man in einer realen React-Anwendung auch immer eine Bibliothek für Routing und Data Fetching brauchen. Beim Laden von Daten von einem Server (zum Beispiel über eine REST API) sei es zudem wahrscheinlich, dass es zu Wasserfällen komme, bei denen Requests aufeinander warten müssten, so dass sich die Anwendung langsam „anfühlen“ würde.

Außerdem sei die Menge an JavaScript-Code, der auf dem Client für eine SPA benötigt wird, problematisch. Der JavaScript-Code müsse schließlich zunächst in den Browser geladen und ausgeführt werden, bevor die Anwendung dargestellt und bedient werden kann. Je nach Qualität der Verbindung kann das zu Verzögerungen beim Ausführen der Anwendung führen, insbesondere da der benötigte JavaScript-Code im Client auch mit jedem neuen Feature der Anwendung wächst.

Aus dieser Argumentation leitet das React-Team den radikalen Schluss ab, dass „vollständige“ Anwendungen nun eben mit einem entsprechenden Fullstack-Framework gebaut werden sollen, weil nur damit die geschilderten Probleme gelöst werden könnten. Hinweise, wie Single-Page-Anwendungen künftig mit React gebaut werden sollen und wie es mit bestehenden React-Anwendungen weitergeht, findet man hingegen so gut wie gar nicht mehr. Auch in anderen öffentlichen Verlautbarungen, etwa in den sozialen Medien, vertritt das React-Team diese Linie vehement und hat damit teilweise heftige Diskussionen ausgelöst, denn auch React wohlgesonnene Entwicklerinnen und Entwickler sehen das Vorgehen aus unterschiedlichen Gründen kritisch.

Der Wechsel zu einem Fullstack-Framework für eine bestehende Anwendung ist nicht trivial (es gibt auch keinen Migrationspfad) und hat erhebliche Konsequenzen für Architektur und Betrieb der Anwendung: JavaScript spielt in den Single-Page-Anwendungen zwar zur Entwicklungszeit und bei der Ausführung im Browser eine Rolle, aber (abgesehen von serverseitig gerenderten Anwendungen) nicht auf dem Server. Das ändert sich mit Fullstack-Frameworks, hier ist ein JavaScript-fähiger Server (wie Node oder Bun) faktisch erforderlich. Ein klassischerWebserver für statischen Content wie bisher reicht nicht mehr aus.

React Server Components

Die serverseitige Ausführung von Teilen der Anwendung wird damit begründet, dass JavaScript-Code im Browser zur Laufzeit eingespart werden kann. Dazu gibt es eine neue Art von Komponenten, die React Server Components (RSC). Diese Komponenten werden entweder zur Buildzeit oder zur Laufzeit auf dem Server gerendert, allerdings nie im Client. Der JavaScript-Code dieser Komponenten gelangt in keinem Fall auf den Browser.

Stattdessen wird – mittels eines proprietären Protokolls – eine Repräsentation der UI dieser Komponenten an den Client geschickt. Dort kümmert sich React dann um das Einbauen bzw. Aktualisieren der UI in die aktuelle Darstellung. Da die Komponenten nicht auf dem Client ausgeführt werden, können sie einerseits die Server- bzw. Buildinfrastruktur nutzen, um zum Beispiel Daten direkt aus einer Datenbank oder vom Dateisystem zu lesen. Andererseits kann im Code dieser Komponenten mit Geheimnissen wie zum Beispiel API Keys verwendet werden, da diese ebenfalls nicht an den Browser gesendet werden und die geschützte Server-Umgebung nicht verlassen.

Die Einführung der Server Komponenten zielt darauf ab, die Darstellung im Browser durch das Einsparen von JavaScript-Code zu beschleunigen. Diese Komponenten können außerdem als asynchrone Funktionen implementiert werden, was mit bisherigen React-Komponenten nicht möglich ist. Dadurch wird der Zugriff auf asynchrone APIs (Datenbanken, HTTP-Aufrufe etc.) vereinfacht, und auch das parallele Laden von Daten aus mehreren Quellen wird damit an Stellen möglich, an denen es mit der klassischen React-Architektur schwierig ist.

In den asynchronen Server Components kann eine Komponente etwa mehrere Requests starten, und die Promise-Objekte dafür an Unterkomponenten weitergeben, die dann auf die Daten warten. Während die Daten geladen werden, kann React feingranulare Platzhalter-Komponenten rendern. Zum Beispiel kann eine Komponente die Daten eines Artikels in einem Request und parallel dazu die Kommentare zu dem Artikel in einem weiteren Request laden. Während die Requests laufen, schickt React bereits eine Platzhalter-Darstellung, zum Beispiel eine Warte-Meldung an den Client. Sobald die Requests dann abgeschlossen werden, rendert die React die entsprechenden Komponenten auf dem Server und schickt die aktualisierte UI zum Client. Wenn die Daten für den Artikel schneller als die Daten für die Kommentare geladen werden, würde React auch zunächst nur die aktualisierte Artikel-Darstellung an den Client senden und später dann die Darstellung für die Kommentare.

Priorisierung von UI-Updates mit Suspense

An welcher Stelle diese „Sollbruchstellen“ verlaufen, also wo React auf das fertige Rendern warten soll, bzw. wo Platzhalter angezeigt werden sollen, kann mit der Suspense-Komponente von React festgelegt werden. Je nach Anforderung rendert React dann zum Beispiel einen Platzhalter, bis alle Komponenten einer Seite ihre Daten erhalten haben und sich rendern konnten. Oder es wird nur ein Teil der Seite mit „wichtigen“ Daten (Artikel) gerendert, aber auf die eher „unwichtigen“ Daten (Kommentare) wird nicht gewartet und hier zunächst ein Platzhalter dargestellt.

Das Listing 1 zeigt ein entsprechendes Beispiel. Die Page-Komponente startet zwei asynchrone Aufrufe und übergibt die entsprechenden Promise-Objekte an die Unterkomponenten Article bzw. CommentList. Diese warten jeweils darauf, dass die Promises aufgelöst werden (mit der Standard JavaScript await API) und rendern dann die Daten.

Solange die beiden Komponenten nicht vollständig gerendert werden können, da die Requests noch laufen, bzw. die Promises noch nicht „fullfilled“ sind, zeigt React die Fallback-Komponente an (ein h1-Element mit einer Meldung, es kann hier aber jede beliebige Komponente angegeben werden). Ein Besucher der Website sieht also die Fallback-Komponente, bis beide Komponenten gerendert wurden („alles oder gar nichts“).

Das zweite Listing  zeigt ein Beispiel, in dem eine Suspense-Komponente um beide Komponenten gelegt wurde. In diesem Fall würde React zunächst die beiden Fallback-Komponenten anzeigen. Sobald eines der beiden Promises die geforderten Daten zurückliefert und die zugehörige Komponente gerendert werden kann, tauscht React den entsprechenden Platzhalter gegen die gerenderte Komponente aus. Ein Besucher der Seite würde also die Artikel bzw. die Kommentare sehen, sobald sie jeweils ermittelt wurden.

Die Suspense-Komponente ist also sehr flexibel. So wäre es auch möglich zu bestimmen, dass in jedem Fall zum Beispiel die CommentList-Komponente nicht vor der Article-Komponente gerendert wird. In dem Fall würde React dann warten, bis die Artikel geladen wurden und die Article-Komponente rendern. Sind zu diesem Zeitpunkt auch schon die Kommentare vorhanden (weil der Request mindestens gleich schnell wie der Request zum Laden der Artikel war), werden auch die Kommentare dargestellt. Sind die Kommentare zu dem Zeitpunkt noch nicht geladen, wird der Kommentare-Platzhalter angezeigt. Umgekehrt würde React die Kommentare aber nicht anzeigen, solange der Artikel noch nicht geladen und gerendert werden konnte:

Da es sich bei den gezeigten Komponenten um Server Components handelt, wird der Code jeweils nur auf dem Server ausgeführt. Nur die (teil-)fertig gerenderten Komponenten werden von React auf den Browser gesendet.

Fullstack-Frameworks Next.js und Remix

Möchte man die neuen React-Features in der eigenen Anwendung verwenden, kommt man um ein Fullstack-Framework in der Praxis nicht herum, da die Implementierung der Serverseite sehr aufwendig ist. Allerdings gibt es bislang nur in Next.js Unterstützung für React Server Components – mit dem „App Router“, der seit der Version 13.4 Bestandteil von Next.js ist. Eine Migration einer bestehenden Anwendung ist allerdings nicht trivial, da Next.js einige Annahmen über eine Anwendung trifft. Insbesondere, wenn man schon einen (clientseitigen) Router im Einsatz hat, ist der Umstieg schwierig bis unmöglich. Hier wird die Zukunft zeigen, inwieweit realistische Migrationsszenarien von Next.js angeboten werden.

Als Alternative zu Next.js wird auf der React Homepage Remix genannt. Dabei handelt es sich um ein Fullstack-Framework, das aus dem React Router hervorgegangen ist, bzw. vom selben Team entwickelt wird. Hier gibt es zwar noch keine Unterstützung für React Server Components, aber auch mit Remix ist es möglich, Code rein serverseitig auszuführen. Dabei handelt es sich in erster Linie nur um den Code zum Laden und Speichern von Daten.

Der JavaScript-Code für Komponenten gelangt mit Remix weiterhin vollständig in den Browser, was sich auch erst mit der Unterstützung für RSC ändern dürfte. Allerdings werden die Komponenten serverseitig (wie in Next.js) vorgerendert, so dass die erste Darstellung im Browser schnell erfolgt. Auch für die weitere Interaktion in der Anwendung verspricht Remix, dass hierfür nur wenig JavaScript ausgeführt werden muss, weil Remix an vielen Stellen Standard Browser APIs (wie zum Beispiel das form-Element) verwendet.

Der Trend zu „Fullstack-Anwendungen“ wird sicherlich in Zukunft an Bedeutung gewinnen, zumal alle Beteiligten, sowohl aus dem React-Team als auch von Next.js bzw. Remix, derzeit verständlicherweise massiv Werbung für ihre Ideen machen. Trotz allem sollte man prüfen, ob die genannten Probleme für die eigenen Anwendungen und Anforderungen relevant sind und die versprochenen Lösungen überhaupt tragen.

Vite als Nachfolger von create-react-app

Fast im Schatten der Fullstack-Diskussion gibt es aber weitere Entwicklungen im React-Ökosystem, die ebenfalls die Single-Page-Anwendungen betreffen. Zum einen ist mit der Fullstack-Empfehlung der Hinweis auf das create-react-app (CRA)-Tool aus der offiziellen React-Dokumentation verschwunden. Mit diesem Tool konnten React-Projekte initial erzeugt werden, ohne dass man sich um die Verwaltung von Abhängigkeiten und die Konfiguration etwa des Build-Stacks kümmern musste. Schon seit längerer Zeit wurde darüber spekuliert, ob dieses Tool überhaupt noch weiterentwickelt wurde, denn auch hier waren die Updates rar. Mittlerweile scheint klar, dass das Tool als eigenständige Lösung wohl keine Zukunft mehr hat und stattdessen zu einer Art „Meta-Tool“ („Launcher“) umgebaut werden soll, das dann an andere Tools (wie z.B. Next.js oder Vite) delegiert.

Zur Entwicklung von Single-Page-Anwendung mit React hat sich stattdessen Vite herausgebildet, das ursprünglich zum Bauen von Vue-Anwendungen gedacht wurde. Mittlerweile ist es aber ein „neutrales“ Build-Tool, für das es unter anderem ein React-Template gibt. Das Aufsetzen von Projekten damit ist ähnlich einfach wie mit create-react-app. Allerdings ist das Template nicht so umfangreich wie create-react-app, bei dem beispielsweise Prettier in der Konfiguration enthalten ist.

Durch die Verwendung von esbuild und einiger weiterer Optimierung ist der Vite-Stack dafür während der Entwicklung deutlicher performanter als create-react-app. Vite bringt ein eigenes Testtool mit (vitest), das weitgehend identisch mit Jest ist. Eine Migration von create-react-app-basierten Anwendungen auf Vite ist bei Bedarf somit grundsätzlich möglich.

Während Server Components als asynchrone Funktionen implementiert werden können, ist das mit herkömmlichen Client-Komponenten nicht der Fall. Um hier die Arbeit mit Promises zu vereinfachen, ist ein neuer Hook in Arbeit, der nur „use“ heißen soll und bereits als experimentelles Feature ausprobiert werden kann. Dem Hook wird ein Promise übergeben, und React wartet mit dem weiteren Rendern der Komponente so lange, bis das Promise aufgelöst ist. Dieses Verhalten ähnelt dem await in den oben gezeigten Beispielen einer asynchronen Server Komponente, und auch hier kann eine Komponente mit Suspense einen Platzhalter anzeigen. Die Details dazu sind allerdings noch unklar.

Suspense in Single-Page-Anwendungen

Möchte man bereits heute in einer Single-Page-Anwendung das Suspense-Feature von React nutzen, kann man dazu sowohl die Bibliothek TanStack Query (ehemals React Query), als auch den React Router (ab Version 6.4) nutzen. In der Version 5 von TanStack Query ist dazu der Hook useSuspenseQuery hinzugekommen. Dieser erhält dieselben Parameter wie der useQuery-Hook, der bereits aus vorherigen Versionen der Bibliothek bekannt ist.

Allerdings sorgt der useSuspenseQuery-Hook dafür, dass die Komponente erst dann gerendert wird, sobald die gewünschten Daten geladen wurden. Damit ist dieser Hook in etwa mit dem await in den Server Components vergleichbar. Ebenfalls wie in den Server Components kann mit der React Suspense-Komponente eine „Sollbruchstelle“ eingezogen werden, an der React das Rendern abbricht, sofern der useSuspenseQuery-Hook noch auf Daten wartet. Ein Beispiel ist im vierten Listing zu sehen:

Der Code läuft hier – wie bei einer Single-Page-Anwendung üblich – vollständig im Client. Davon abgesehen entspricht das Verhalten des Codes dem letzten Beispiel mit RSC (siehe Listing 3).

Auch der React Router bietet mittlerweile Unterstützung für das Laden von Daten, etwa beim Wechseln einer Route. Dazu kann in der Routen-Definition eine loader-Funktion angegeben werden. Wird die entsprechende Route im Browser aufgerufen, sorgt der Router dafür, dass die Daten mit der loader-Funktion geladen werden. Dabei kann diese entscheiden, ob die Route erst dann gerendert wird, wenn alle Daten geladen wurden.

Alternativ kann die loader-Funktion ein Promise zurückliefern, auf das beim Rendern mit der Async-Komponente des Routers gewartet wird. In diesem Fall muss mit der Suspense-Komponente wie in den vorherigen Beispielen eine Fallback-Komponente festgelegt werden.

Auf die geladenen Daten kann dann innerhalb einer Komponente mit dem Befehl useLoaderData– bzw. useAsyncLoaderData zugegriffen werden. Das bereits bekannte Beispiel in der React Router-Variante ist in Listing 5 zu sehen:

Übrigens macht der React Router keine Aussage darüber, wie die Daten in der loader-Funktion technisch geladen werden müssen. Grundsätzlich kann darin deswegen auch die TanStack Query Bibliothek benutzt werden, um zum Beispiel von deren Caching-Features zu profitieren. Ob einem die Idee, das Routing mit dem Laden und Speichern von Daten zu verbinden, grundsätzlich gefällt und man dieses Verfahren nutzen möchte, steht natürlich auf einem anderen Blatt.

Neben den gezeigten Features unterstützen TanStack Query und der React Router auch das klassische serverseitige Rendern (SSR), so dass hiermit die Anwendung bei Bedarf auf dem Server in fertigen HTML-Code vorgerendert werden kann.

React 2024 – Fazit

Insgesamt gibt es also auch außerhalb der „Fullstack-Bewegung“ eine Menge Neuerungen und Änderungen im React-Umfeld, die das Arbeiten insbesondere mit Daten erheblich vereinfachen. Mit Vite steht außerdem ein ausgereiftes und modernes Build-Tool als Alternative zu create-react-app zur Verfügung.

Wer heute nicht unmittelbar auf ein Fullstack-Framework umstellen möchte oder kann, kann damit also noch warten, die weitere Entwicklung verfolgen und dank der modernen Bibliotheken zumindest von einem Teil der neuen React-Features auch in klassischen Single-Page-Anwendungen profitieren.

Titelmotiv: Photo by Lautaro Andreani on Unsplash

Nils Hartmann

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten