Über unsMediaKontaktImpressum
Nils Hartmann 27. Dezember 2023

Next Generation Webapps? Fullstack-Anwendungen mit React

Das React-Team empfiehlt zur Entwicklung von Anwendungen künftig den Einsatz eines Fullstack-Frameworks. Dieser Artikel beleuchtet die Hintergründe der Empfehlung und stellt als exemplarischen Vertreter dieser Frameworks Next.js vor.

Beim Aufruf einer statischen Webseite wie beispielsweise einer Nachrichten- oder Produktseite wird bei jedem Request eines Clients vom Server ein vollständiges HTML-Dokument heruntergeladen. Dieses Dokument zeigt der Browser unmittelbar an, was in der Regel sehr schnell geht. Bei der Navigation durch die Webseite wird bei jedem Seitenwechsel dann eine neue Seite vom Server abgefragt, heruntergeladen und vom Browser dargestellt. Dabei spielt es aus Client-Sicht keine Rolle, ob die Seite bei einem Aufruf jedes Mal auf dem Server generiert wird (zum Beispiel, um benutzerspezifische Informationen einzubinden) oder ob es sich um ein fertiges HTML-Dokument handelt.

Entscheidend für den Browser ist, dass es sich um ein vollständiges Dokument handelt, das mit jedem Request vollständig ersetzt wird. Diese Art von Webseiten ist seit vielen Jahren bekannt, hat aber die Schwäche, dass Interaktionsmöglichkeiten sehr eingeschränkt sind: Das Austauschen nur eines Teils einer Seite beispielsweise ist in diesem Modell nicht möglich. Auch komplexe, client-seitige Validierungen eines Formulars sind nicht möglich. Für jegliche Interaktion, die auf dem Client stattfinden soll, muss der Browser zwangsläufig JavaScript-Code ausführen, der nun für einen Teil der Darstellung zuständig ist. Damit kann zum Beispiel bereits vor dem Absenden eines Formulars ein Hinweis eingeblendet werden ("Die eingegebene PLZ ist ungültig") oder das versehentliche, doppelte Absenden eines Formulars verhindert werden. Dieser Ansatz ist zunächst ohne große technische Hürden umsetzbar, da das Einbinden von JavaScript-Code vergleichsweise trivial ist, der Server JavaScript-Dateien wie andere statische Assets behandelt und auf Anfrage an den Browser ausliefern kann. Problematisch wird dieser Ansatz, wenn immer mehr solcher "JavaScript-Schnipselchen" der Webseite hinzugefügt werden. In diesem Fall kann es schnell zu chaotischem, unverständlichem und schwer wartbarem Code kommen. Verstärkt wird dieses Problem, wenn im Backend normalerweise eine andere Sprache als JavaScript zum Einsatz kommt. In diesem Fall gibt es auch ein tendenziell schwer wartbares Gemisch von Frontend-Sprache und -Ökosystem (JavaScript) und der im Backend eingesetzten Technologien.

An dieser Stelle setzen die modernen Single-Page-Anwendungen an. Hier wird nun die Darstellung im Browser vollständig von JavaScript übernommen. Aus UI-Sicht ist der Server nur noch für die Auslieferung von JavaScript (und anderen statischen Assets wie CSS- oder Bild-Dateien) verantwortlich. Außerdem stellt der Server in der Regel eine oder mehrere APIs zur Verfügung, über die der Client Daten laden und speichern kann (z. B. REST oder GraphQL). Damit ist eine saubere Trennung der Verantwortlichkeiten zwischen Front- und Backend gegeben und für die Entwicklung des Frontends stehen leistungsfähige und stabile Frameworks wie React, Angular und Vue zur Verfügung.

Eine Konsequenz dieses Ansatzes ist es, dass die initiale Darstellung einer Seite länger dauert als bei einer Seite, die serverseitig erzeugt wurde. Bei einer SPA muss der Browser zunächst den JavaScript-Code der Anwendung laden, interpretieren und ausführen. Erst bei der Ausführung des JavaScript-Codes wird die Anwendung dann dargestellt. Erschwerend kann hinzukommen, dass auch die für die Darstellung benötigten Daten (z. B. Produktinformationen) erst bei der Ausführung dieses Codes geladen werden. Hier kommt es also zu einer Art Wasserfall (1. Laden des JS-Codes, 2. Interpretieren und Ausführen des JS-Codes, 3. Laden der Daten, 4. Aktualisieren der Darstellung, sobald die benötigten Daten geladen wurden), auf Grund dessen die Darstellung üblicherweise langsamer ist als bei einer serverseitig gerenderten Anwendung. Wie viel langsamer eine solche Anwendung ist, hängt von einer Reihe von Faktoren ab, nicht zuletzt von der Internetverbindung und der Performance der JS-Runtime im Browser. Auch die Frage, ob die geringere Performance überhaupt ein Problem darstellt, hängt von Anwendung und Anwendungsfall ab. Eine Marketingseite kann andere Performance-Ansprüche als zum Beispiel eine In-House-Anwendung haben.

Zur Kompensation dieses Performance-Nachteils entstehen Fullstack-Frameworks (auch Meta-Frameworks genannt), mit denen sich – wie bei Single-Page-Anwendungen – ganze Anwendungen für den Browser bauen lassen. Im Gegensatz zu einer "reinen" SPA und ähnlich wie bei einer statischen Webanwendung kann das Fullstack-Framework die Oberfläche aber bereits auf dem Server rendern und fertiges HTML zum Client schicken. Damit ist die Darstellung vergleichbar so schnell wie bei einer statischen Website. Um aber – anders als bei statischen Websites – feingranulare und schnelle Interaktionen zu ermöglichen, kann das Framework im weiteren Verlauf der Anwendung auch einzelne Teile der Anwendung austauschen und mittels JavaScript aktualisieren. Im Gegensatz zur klassischen Single-Page-Anwendung wird dafür allerdings nicht der komplette JavaScript-Code der Anwendung im Browser benötigt, sondern nur der Code, der für interaktive Teile verantwortlich ist. Gewissermaßen entspricht dieses Verhalten dem "JavaScript-Schnipselchen"-Ansatz. Allerdings ist bei den Fullstack-Frameworks die Anwendung (wie bei einer SPA) aus einem Guss – mit einer Programmiersprache (JavaScript) und Tech-Stack gebaut. Die benötigten Schnipselchen werden automatisch vom Framework aus dem Code der Gesamtanwendung herausgelöst und zum Client geschickt. Damit – so das Versprechen der Fullstack-Frameworks – soll das Beste aus beiden Welten erreicht werden. Einerseits eine schnelle Darstellung insbesondere beim Aufrufen der Anwendung, andererseits aber die Möglichkeit der feingranularen Interaktion.

Aus diesem Grund empfiehlt React auch seit Frühjahr 2023 offiziell den Einsatz eines solchen Frameworks, namentlich aktuell Next.js oder Remix. Bevor man den Sprung auf ein solches Framework wagt, sollte man sich aber genau anhand der Anforderungen der eigenen Anwendung überlegen, inwiefern die geschilderten Probleme für einen selbst überhaupt zutreffen. Denn auch wenn – wie im Folgenden zu sehen sein wird – die Entwicklung von Anwendungen mit Next oder Remix sehr ähnlich den SPA-Anwendungen mit React ist, gibt es den gravierenden Unterschied, dass faktisch zur Laufzeit (zusätzlich zum vorhandenen Backend) ein javascript-basierter Server eingesetzt werden muss, auf dem das Fullstack-Framework dann läuft. Das kann lokal oder in der Cloud erfolgen, ist aber möglicherweise eine große Umstellung für Architektur und Betrieb einer bestehenden Anwendung.

Die Wahl des passenden Frameworks

Hat man sich für den Einsatz eines Frameworks entschieden, stellt sich die Frage, welches man verwenden möchte. In der React-Landschaft sind zurzeit zwei Frameworks populär und werden auch beide explizit in der React-Dokumentation erwähnt: Next.js von Vercel und Remix, dessen Entwicklungsteam mittlerweile bei Shopify arbeitet. Abgesehen davon, dass Next.js weiterverbreitet sein dürfte als Remix, unterscheiden sich die beiden Frameworks auch von der technischen Herangehensweise, um die oben geschilderten Probleme zu lösen. Next.js setzt auf die React Server Components (RSC), die in diesem Jahr erstmals als stabil bezeichnet wurden. Remix unterstützt diese zurzeit noch nicht, so dass hier eine proprietäre API verwendet werden muss.

Next.js unterstützt React Server Components ab der Version 13.4 mit dem in der Version eingeführten "App-Router". Bei diesem Router wird mit unterschiedlichen Datei- bzw. Verzeichnis-Konventionen die URL- bzw. Routen-Struktur der Anwendung abgebildet. Dazu wird für jedes Segment einer URL ein Unterverzeichnis angelegt, das den Namen des gewünschten URL-Segments trägt. In dem Verzeichnis muss dann eine Datei page.tsx liegen, die eine React-Komponente exportiert. Diese Komponente wird aufgerufen, wenn der entsprechende Pfad vom Browser abgefragt wird. (URL: /“hello/world“ -> Pfad „/app/hello/world/page.tsx“). Diese Konvention erinnert an statische Webserver, in denen eine index.html-Datei in einem Verzeichnis dazu führt, dass ein gleichnamiger Pfad im Browser verwendet werden kann. Neben der page.tsx-Datei gibt es einige weitere Dateien, die in Next.js eine feste Bedeutung haben, zum Beispiel loading.tsx, die verwendet wird, während Next.js Daten lädt. Um auszudrücken, dass ein URL-Segment dynamisch ist und zur Laufzeit unterschiedliche Werte enthalten kann (z. B. eine Produkt-ID), wird der Verzeichnisname in eckigen Klammern geschrieben. Der Wert, der sich zur Laufzeit bei einem Request an dieser Stelle befindet, wird der Page-Komponente dann per React-Property übergeben.

Das Listing 1 zeigt dazu ein kleines Beispiel. Hier gibt es drei Routen ("/", "/posts" und "/posts/[postId]"), mit denen später eine Landing-Page ("/"), eine Übersicht über Blog-Artikel ("/posts") und ein einzelner Blog-Post ("/posts/P1") dargestellt werden sollen. Über die Link-Komponente von Next.js kann die Navigation zwischen den Seiten realisiert werden. Die Implementierung der Komponenten wird im Folgenden dann vervollständigt. Zur besseren Lesbarkeit wird in allen Listings auf die Angabe von TypeScript-Typen verzichtet.
 
Listing 1: Server-Komponenten mit Next.js

// /app/page.tsx
import Link from "next/link";
 
export default function LandingPage() {
  return (
    <main>
      <h1>Simple Blog</h1>

      <Link href={"/posts"}>Blog Posts</Link>
    </main>
  );
}
 
// /app/posts/page.tsx
import Link from "next/link";

export default function PostListPage() {
  return (
    <section>
      <h1>Blog Posts</h1>

      <ul>
        <li>
          <Link href={"/posts/P1"}>Learn React</Link>
        </li>
        <li>
          <Link href={"/posts/P2"}>Next.js fundamentals</Link>
        </li>
        <li>
          <Link href={"/posts/P3"}>TypeScript advanced</Link>
        </li>
      </ul>
    </section>
  );
}
 
// /app/posts/[postId]/page.tsx
export default function BlogPostPage({ params }) {
  return (
    <article>
      <h1>Blog Post with postId {params.postId}</h1>
    </article>
  );
}

Die in Listing 1 gezeigten Komponenten sehen wie "normale" React-Client-Komponenten aus, sind aber React Server Components, da Next.js per Default alle Komponenten als Server Components behandelt. React-Server-Komponenten werden explizit nicht im Client ausgeführt. Aus Code-Sicht sind die beiden Arten von Komponenten nahezu identisch, allerdings wird der JavaScript-Code der RSC nicht an den Browser übermittelt. Die RSC werden entweder im Build-Prozess oder bei einem Request im Server gerendert und dann fertig gerendert an den Browser übertragen. Hierbei kommt allerdings kein HTML zum Einsatz, sondern ein react-proprietäres Format, das im Client dann von React bzw. Next.js in die Anwendung eingesetzt wird.

Im Unterschied zu Client-Komponenten können Server-Komponenten als asynchrone Funktionen implementiert werden. Damit können sie direkt mit asynchronen APIs arbeiten, zum Beispiel, um Daten aus einer Datenbank oder einem remote HTTP-Endpunkt abzurufen. Das Listing 2 zeigt dieses Verhalten exemplarisch am Beispiel der Blog-Übersicht. Während die Daten auf dem Server geladen werden, kann React diese Komponente natürlich nicht rendern und folglich auch nichts darstellen. Aus diesem Grund kann in Next.js eine zweite Datei angelegt werden, die loading.tsx heißt. Hieraus wird eine Komponente exportiert, die automatisch so lange dargestellt wird, wie das Promise in der eigentlichen Komponente noch nicht aufgelöst wurde. Diese Platzhalterkomponente wird unmittelbar nach dem Aufruf der Route fertig gerendert an den Browser gesendet. Sobald die eigentliche Komponente alle ihre Daten abfragen konnte (bzw. die Promises aufgelöst wurden), rendert React die Komponente, sendet sie an den Browser und tauscht dort den Platzhalter gegen sie aus.

Listing 2: Eine asynchrone Server-Komponente

// /app/posts/page.tsx
export default async function PostListPage() {
  // Zugriff auf Datenbank, HTTP-Endpunkt o.ä.
  const posts = await loadPosts();

  return (
    <section>
      <h1>Blog Posts</h1>

      <ul>
        {posts.map((p) => (
          <li key={p.id}>
            <Link href={`/posts/${p.id}`}>{p.title}</Link>
          </li>
        ))}
      </ul>
    </section>
  );
}
 
// /app/posts/loading.tsx
export default function Loading() {
  return <h1>Blog posts loading - stay tuned!</h1>
}

Dieses Verhalten funktioniert nicht nur für ganze Seiten, sondern auch für einzelne Bereiche einer Seite. Dazu wird die Suspense-Komponente von React verwendet, die um eine oder mehrere Komponenten gelegt werden kann. Während in einer der umschlossenen Komponenten auf ausstehende Promises gewartet wird, rendert React an der Stelle ebenfalls eine Platzhalterkomponente, die der Suspense-Komponente übergeben wird. Auch hier sendet React dann die fehlende Komponente an den Client nach, sobald diese fertig gerendert werden konnte. Das Listing 3 zeigt dieses Verhalten am Beispiel der Darstellung eines einzelnen Blog-Posts. Hier werden neben den Daten für den Blog-Post auch Kommentare geladen. Um das Laden der Daten möglichst effizient zu gestalten, werden beide Requests parallel gestartet. Um die aus fachlicher Sicht wichtigsten Daten (Blog-Post) aber sofort nach deren Erhalt darstellen zu können, soll React mit deren Darstellung nicht warten, bis auch die zugehörigen Kommentare geladen worden sind. Dazu wird die Comment-Komponente mit Suspense umschlossen. Sobald React die Seite (im Zweifel ohne Kommentare) rendern konnte, sendet React diesen Teil der Seite mit dem in Suspense angegebenen Platzhalter an den Client zurück. Die gerenderten Kommentare werden dann später nachgesendet.

Listing 3: Der Einsatz von Suspense

// /app/posts/[postId]/page.tsx

export default async function BlogPostPage({ params }) {
 
  const commentsPromise = loadComments(params.postId);
  const post = await loadPost(params.postId);
 
  return (
    <article>
      <h1>{post.title}</h1>
      {post.body}
     
      <React.Suspense fallback={<h2>Comments loading...</h2>}>
        <Comments commentsPromise={commentsPromise} />
      </React.Suspense>
     
    </article>
  );
}

Die bis hier gezeigten Komponenten werden von Next.js entweder zur Build- oder zur Laufzeit gerendert. Per Default rendert Next.js bereits alle Komponenten im Build, sofern darin keine dynamischen Informationen (z. B. dynamische URL-Segmente, Cookies oder HTTP-Header) verwendet werden. Das trifft auf die Landing-Page und die Blog-Übersicht zu. Die Route /posts/[postId] enthält aber ein dynamisches Segment ("postId") und wird daher bei jedem Request gerendert, da Next.js nicht ohne weiteres im Build alle möglichen Werte ermitteln kann, die an die URL für den postId-Parameter zur Laufzeit übergeben werden könnten. Um auch diese Seiten bereits zur Buildzeit zu rendern, kann eine Funktion implementiert werden, mit der Next.js die möglichen Werte für postId mitgeteilt werden. Ob dieses Feature nützlich und praktikabel ist, hängt unter anderem davon ab, wie häufig sich die Seiten ändern, aber für Webseiten mit viel statischem Content, der sich eher selten ändert (zum Beispiel Darstellung von Produkten), kann dieses Feature nützlich sein.

Eine Folge des Renderns während der Build- oder Laufzeit auf dem Server ist, dass die Server Components keinen React State haben können. Das hat eine Reihe von Konsequenzen. Die Liste der Blog-Posts beispielsweise soll vom Benutzer der Website sortierbar sein. In einer klassischen React-Anwendung würde dazu womöglich eine neu sortierte Liste vom Server abgefragt, in den State gesetzt und dann gerendert werden. Mangels State in der Server-Komponente entfällt diese Variante. Stattdessen muss ein Request in das Next.js-Backend gemacht werden, der dazu führt, dass die Seite (oder zumindest ein Teil der Seite) neu gerendert wird. Das Listing 4 zeigt eine entsprechend erweiterte Post-List-Komponente, die zwei Links rendert. Beim Klicken auf einen der beiden Links wird die URL geändert (Query-Parameter). Dies führt – wie bei einer klassischen Webseite – dazu, dass ein Server Request ausgeführt wird. Allerdings wird hier nicht eine komplett neue Seite geladen. Stattdessen sorgt Next.js dafür, dass nur die veränderten Teile der Website ausgetauscht werden.

Listing 4: Aktualisieren der Darstellung einer Seite

// /app/posts/page.tsx
export default async function PostListPage() {

  // ...unverändert...

  return (
    <section>
     
      <nav>
        <Link href={"/posts?order_by=title"}>Order by title</Link>
        <Link href={"/posts?order_by=date"}>Order by date</Link>
      </nav>

      <ul>
        {posts.map((p) => (
          <li key={p.id}>
            <Link href={`/posts/${p.id}`}>{p.title}</Link>
          </li>
        ))}
      </ul>
    </section>
  );
}

Server-Komponenten können außerdem nicht auf Ereignisse im Browser (Events) reagieren. Sobald eine Komponente Zustand benötigt oder auf Ereignisse reagieren möchte, muss sie als "klassische" Client-Komponente implementiert werden. Diese kann innerhalb von Server-Komponenten verwendet werden. Trifft Next.js beim Rendern einer Server-Komponente auf eine Client-Komponente, wird das Rendern des Komponenten-Baums an dieser Stelle abgebrochen und lediglich eine Information an den Client gesendet, welche Komponente an dieser Stelle darzustellen ist.

Next.js bringt von Haus aus ein sehr aggressives Caching auf verschiedenen Ebenen mit.

Für Client-Komponenten wird – wie bisher in klassischen React-Anwendungen – der JavaScript-Code an den Browser übertragen. Im Browser kann React damit die Komponente dann rendern und an der korrekten Stelle im Komponentenbaum einbinden. Da in Next.js per Default alle Komponenten Server-Komponenten sind, muss eine Client-Komponente explizit gekennzeichnet werden, wozu die "use client"-Direktive am Anfang einer JavaScript-/TypeScript-Datei verwendet wird. Alle Komponenten in der Datei und in den Komponenten, die von diesen Komponenten verwendet werden, werden dann von Next.js als Client-Komponenten angesehen. Mit "use client" kann also gewissermaßen die Grenze zwischen Server und Client gezogen werden. Das Listing 5 zeigt ein einfaches Beispiel. Hier verwendet die Blog-List-Komponente nun zwei Buttons (anstelle der Links), um die Query-Parameter der URL zu verändern und somit die Liste umzusortieren. Um weiterhin möglichst viel UI serverseitig zu rendern und möglichst wenig JS-Code in den Browser übertragen zu müssen, ist es ratsam, die Client-Komponenten so tief wie möglich in der Komponentenhierarchie anzusiedeln. Aus diesem Grund ist in Listing 5 auch nur der Order-Button als Client-Komponente ausgezeichnet.

Listing 5: Eine Client-Komponente

// /app/posts/OrderButton.tsx
"use client";

import { usePathname, useRouter, useSearchParams } from "next/navigation";

export default function OrderButton({ orderBy, children }) {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const disabled = searchParams.get("order_by") === orderBy;

  const handleClick = () => {
    const newParams = new URLSearchParams(searchParams);
    newParams.set("order_by", orderBy);
    router.push(`${pathname}?${newParams.toString()}`);
  };

  return (
    <button onClick={handleClick} disabled={disabled}>
      {children}
    </button>
  );
}
 
// /app/posts/page.tsx

export default async function PostListPage() {
  // ...unverändert...

  return (
    <section>
      <h1>Blog Posts</h1>

      <nav>
        <OrderButton orderBy="title">Order by title</OrderButton>
        <OrderButton orderBy="date">Order by date</OrderButton>
      </nav>
      {/* ...unverändert ... */}

    </section>
  );
}

Durch die Trennung von Server- und Client-Komponenten bzw. durch das Rendern von Komponenten auf dem Server ergeben sich auch beim Ändern von Daten Konsequenzen für die Anwendung, zum Beispiel nach dem Absenden eines Formulars. Auch hier muss – ähnlich wie beim Ändern der Sortierreihenfolge – der Server angewiesen werden, eine aktualisierte Darstellung für die geänderten Daten zu senden. Hierfür gibt es allerdings keine React-API, so dass auf die proprietäre Next.js-API zurückgegriffen werden muss. Next.js bringt von Haus aus ein sehr aggressives Caching auf verschiedenen Ebenen mit. So werden beispielsweise im Server alle gerenderten Seiten per Default gecacht. Auch Daten, die im Backend über die fetch-API gelesen werden, werden automatisch gecacht. Zum Aktualisieren des Caches gibt es verschiedene Strategien. Unter anderem kann bei einem fetch-Aufruf ein Tag vergeben werden. Dieser Tag kann in anderen Teilen der Anwendung verwendet werden, um Next.js mitzuteilen, dass die gelesenen Daten aus diesem Aufruf nicht mehr gültig sind und alle Seiten(-teile), die diese Daten verwenden, beim nächsten Abfragen neu gerendert werden müssen.

Die server-seitige Verarbeitung eines abgesendeten Formulars kann in React mit einer "Server Action" erfolgen. Dabei handelt es sich um eine reguläre asynchrone Funktion, die im Backend implementiert ist. Für diese Funktion stellt Next.js automatisch einen Endpunkt zur Verfügung, so dass die Funktion aus dem Frontend heraus aufgerufen werden kann. Dazu kann die Funktion als Argument beispielsweise dem action-Property eines form-Elements übergeben werden. Wird das Formular submitted, serialisiert React das FormData-Objekt, das den Inhalt des Formulars enthält, sendet dieses an den Endpunkt und ruft dann serverseitig damit die Server-Action-Funktion auf. Dieses Verhalten ist dem nativen Verhalten von HTML-Formularen nachgebaut. Allerdings wird beim Submit eines natives Formulars stets eine komplett neue Seite vom Server angefragt. Im Fall einer Server Action sorgt React dafür, dass nur die Daten des Formulars an den Browser übertragen und dann ledigleich die veränderten Teile der Seite zurück an den Client geschickt werden. Das Listing 6 zeigt exemplarisch die Verwendung einer Server Action am Beispiel eines sehr einfachen Formulars zum Eingeben eines Kommentars zu einem Artikel.

Das Formular ist in der CommentBox-Komponente implementiert. Auch dabei handelt es sich übrigens um eine Server-Komponente, die ohne JavaScript zur Laufzeit auskommt. Formulare mit komplexeren Anforderungen können allerdings auch als Client-Komponente gebaut werden, um zum Beispiel lokalen State zu verwenden und dadurch visuelle Hinweise für den Benutzer direkt beim Tippen anzuzeigen. Die CommentBox-Komponente ruft die Server Action addComment auf, die den eingegebenen Kommentar speichert.

Listing 6: React Server Actions

// /app/posts/[postId]/post-actions.ts
"use server";

import { revalidatePath } from "next/cache";

async function saveComment(postId: string, comment: string) {
  // Daten speichern (DB, REST/GraphQL Endpunkt, ...)
  // ...
}

export async function addComment(formData) {
  const postId = formData.get("postId");
  const comment = formData.get("comment");

  await saveComment(postId, comment);

  revalidatePath(`/posts/${postId}`);
}
 
// /app/posts/[postId]/page.tsx
import { addComment } from "./post-actions";

export default async function BlogPostPage({ params }) {
  // unverändert

  return (
    <article>
        { /* ... */ }
        <Comments commentsPromise={commentsPromise} />
        <CommentBox postId={params.postId} />
    </article>
  );
}

function CommentBox({ postId }) {
  return (
    <form action={addComment}>
      <input type="hidden" name="postId" value={postId} />
      <input type="text" name="comment" />
      <button type="submit">Add!</button>
    </form>
  );
}

Fazit

Die Einführung von React Server Components und die Verwendung von Next.js erschließt neue Möglichkeiten für React-Anwendungen, bzw. bietet Verbesserungen für Anwendungsfälle, die bisher mit Single-Page-Anwendungen nicht optimal umgesetzt werden konnten. Ob sich der Einsatz lohnt, hängt allerdings von den individuellen Anforderungen der Anwendung ab und sollte entsprechend kritisch überprüft werden. In welchem Maße die Menge an JavaScript-Code, die der Browser herunterladen und ausführen muss, eingespart werden kann, hängt wesentlich davon ab, wie viele interaktive Teile die Anwendung hat. Je mehr statischer Content, desto mehr JavaScript kann eingespart werden. Auf der anderen Seite bedeutet das Rendern auf dem Server nicht zwangsläufig, dass insgesamt Netzwerkverkehr gegenüber einer Single-Page-Anwendung eingespart wird. Fertig gerenderte Seiten und Seitenteile können ein höheres Datenvolumen verursachen als zum Beispiel Daten, die in einer Single-Page-Anwendung per REST-API gelesen werden.

Zudem ist die Technik in React noch jung, so dass es damit kaum Erfahrungen gibt. Gelernte React-Pattern müssen teilweise neu gelernt werden und auch Next.js selbst ist nicht fehlerfrei, dafür aber sehr meinungsstark, was die Art und Weise angeht, wie damit Anwendungen zu bauen sind. Der Einsatz von RSC hat grundlegende Konsequenzen für Architektur und Betrieb der Anwendung, die nicht immer auf den ersten Blick offensichtlich sind. Nicht zuletzt sollte man im Hinterkopf behalten, dass vergleichbare Versuche, "Fullstack-Frameworks" in anderen Programmiersprachen zu bauen, schon mehrfach in der Versenkung verschwunden sind (z. B. Java Server Faces oder Apache Wicket), weil sie sich in der Praxis als zu komplex erwiesen haben. Nicht zuletzt aus diesem Grund wird der Fullstack-Trend mit viel Skepsis beäugt. Ob zu Recht, wird die Zukunft zeigen.

Autor

Nils Hartmann

Nils Hartmann ist freiberuflicher Software-Entwickler, -Architekt. Er unterstützt Teams bei der Entwicklung von Backend- und Frontend-Anwendungen mit den Schwerpunkten Java, Spring, TypeScript und React.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben