Blog

React 18 unter der Lupe: Concurrent Rendering und weitere Neuigkeiten

Jul 20, 2022

React 18 Neue feature

Concurrent Rendering

React 18 ist erst das zweite Major-Release der populären JavaScript-Bibliothek innerhalb von fast fünf Jahren. Im Gegensatz zum Vorgängerrelease hat sich in dieser Version nun aber einiges getan und es verspricht auch weitere spannende Neuerungen in der Zukunft.

Die beiden vergangenen Major-Releases von React sind 2020 als React 17 bzw. 2017 als React 16 veröffentlicht worden, wobei das Release 17 offiziell als „Release ohne neue Features“ angekündigt wurde. Es wurden lediglich einige APIs entfernt, die zuvor schon als deprecated markiert worden waren. Der Umstieg von React 16 auf 17 bedeutete also in erster Linie das Erhöhen der Versionsnummer in der Abhängigkeitsbeschreibung eines Projekts. Für React 16 hingegen wurde eine ganze Reihe von Minor-Versionen veröffentlicht, die allerdings nach und nach alle Features hinzufügten, die moderne React-Entwicklung heute ausmacht: das Context API etwa, das Hooks API, Error Boundaries und Code Splitting mit Lazy Loading. Darin zeigt sich eine der Stärken von React: die Releasezyklen sind lang und die APIs sehr stabil. Selbst das Hooks API, das ein komplett neues Programmiermodell für React hinzufügte, wurde abwärtskompatibel in einer Minor-Version hinzugefügt. Bestehende Anwendungen mussten nicht verändert werden und konnten bzw. können Schritt für Schritt bei Bedarf auf das Hooks API umgestellt werden.

Concurrent React

Im Jahr 2018, nach Erscheinen von React 16, wurde die „React Roadmap“ veröffentlicht, auf der unter anderem das Hooks API und das Code Splitting angekündigt wurde. Beide Features sind längst umgesetzt. Aber daneben standen auch der „Concurrent Mode“ und „Suspense for Data Fetching“ auf der Agenda. Der ursprüngliche Plan sah vor, diese beiden Features noch im Laufe des Jahres 2019(!) zu veröffentlichen. Offenbar war die Umsetzung dann aber doch komplexer als gedacht, zumal eine der Anforderungen war, weiterhin abwärtskompatibel zu bleiben. Nun aber, knapp vier Jahre später, ist es so weit: Im April wurde React 18 veröffentlicht und damit offiziell „Concurrent React“, wie die Unterstützung für Concurrent Rendering jetzt genannt wird. Durch das Concurrent Rendering kann React das Rendern von Komponenten unterbrechen, um dringendere Renderings vorzuziehen. Ohne Concurrent Rendering ist das Rendering von React-Anwendungen atomar, entspricht also dem bisherigen Verhalten. Das bedeutet, dass zum Beispiel nach einer State-Änderung die ganze Komponentenhierarchie abwärts neu gerendert wurde. Erst wenn das Neurendern komplett erfolgt ist, wird die Änderung in das DOM übernommen (committed) und die Anwendung kann weiterlaufen.

Mit Concurrent Rendering ändert sich dieses Verhalten. React kann dadurch dringende Änderungen weniger dringenden vorziehen. Ein häufig angeführtes Beispiel sieht so aus, dass Benutzereingaben möglichst in Echtzeit sichtbar sein bzw. aktualisiert werden sollen. Das wäre ein „wichtiges“ oder „dringendes“ (urgent) Update. Ein weniger wichtiges Update könnte sein, dass eine Tabelle mit einem Suchergebnis aktualisiert wird. Wird in das Textfeld der Suchbegriff eingegeben, sollte dieser bei einer Eingabe möglichst in Echtzeit aktualisiert werden. Die Tabelle mit dem Suchergebnis darunter könnte aber, wenn nicht ausreichend Ressourcen für die Aktualisierung beider Ansichten vorhanden sind, leicht zeitversetzt erfolgen. Es geht hier also um die bessere Priorisierung von CPU-Ressourcen. Bei fehlenden Ressourcen, z. B. langsamer CPU auf älteren Mobilgeräten, kann React die Renderings priorisieren, die für einen Benutzer besonders wichtig sind. Dabei handelt es sich im Grunde um einen „Trick“ zur Verbesserung der User Experience, denn die Anwendung wird dadurch insgesamt nicht schneller, fühlt sich aber für den Benutzer möglicherweise schneller an. Für viele Anwenderinnen und Anwender fühlt es sich nicht richtig an, wenn Eingaben verzögert erscheinen, aber ein kleiner Versatz bei der Aktualisierung einer Tabelle wird in der Regel akzeptiert bzw. als „natürlich“ wahrgenommen.

Das React-Team geht nach eigenen Angaben davon aus, dass das Concurrent Rendering in den allermeisten Anwendungen out of the box funktioniert, bzw. dass dieses neue Feature keine Probleme in bestehenden Anwendungen macht. Dennoch hat man sich für die sicherste Option entschieden, und das Concurrent Rendering als Opt-in-Feature gebaut, d. h., Anwendungen, die dieses Feature verwenden wollen, müssen es explizit einschalten. Dabei sieht dann der Startprozess der Anwendung etwas anders als bisher aus. Statt ReactDOM.render() wird nun ein Root-Objekt erzeugt, in das die Anwendung gerendert wird. Die neue render-Funktion kommt nun aus dem Modul react-dom/client und nicht mehr direkt aus react-dom. Ein Beispiel dafür ist in Listing 1 zu sehen.

Listing 1: Die neue createRoot-Funktion

import ReactDOM from "react-dom/client";
import App from "./App";
 
const root = ReactDOM.createRoot(
  document.getElementById("root")
);
root.render(<App />);

Übrigens gibt es auch für das serverseitige Rendern zwei neue APIs, die das Streamen von HTML-Code zum Client ermöglichen, sodass nicht – wie bisher – immer erst ein kompletter HTML-String gebaut werden muss, der dann komplett zum Server geschickt wird.

Updates priorisieren mit useTransition

Um innerhalb einer Anwendung zu kennzeichnen, dass ein Update eine geringere Dringlichkeit hat, das Rendering dafür im Zweifel also pausiert oder sogar verworfen werden kann, kann eine sogenannte Transition beschrieben werden, ein Übergang der UI von A nach B. Dazu gibt es den useTransition-Hook, der zwei Parameter zurück liefert: eine Funktion zum Starten der Transition (startTransition) und eine Information zum Status der Transition (isPending).

Die Funktion zum Starten der Transition wird verwendet, um anzuzeigen, dass eine bestimmte State-Änderung eine geringere Priorität hat. Dazu wird eine Funktion übergeben, in der ein oder mehrere Setter-Aufrufe zum Verändern des Zustands durchgeführt werden. React wird dann die Komponente wie gewohnt neu rendern, allerdings andere Updates, die während des Renderns auftreten, bevorzugen. Der zweite Parameter, isPending, zeigt an, ob diese Transition gerade läuft, also dass das State-Update bzw. dass das Rendern noch nicht abgeschlossen ist.

Listing 2: Der useTransition Hook

function SearchResults({ searchString }) {
  // stellt eine sehr große Tabelle da,
  // deren Rendering lange dauert
 
  return <>...</>;
}
 
function App() {
  const [input, setInput] = useState("");
  const [searchString, setSearchString] = useState("");
  const [isPending, startTransition] = useTransition();
 
  function handleInputChange(newValue) {
    // "dringendes" Update, wird sofort gerendert
    setInput(newValue);
 
    startTransition(() => {
      // weniger "dringendes" Update, wird ggf. später
      // gerendert
      setSearchString(newValue);
    });
  }
 
  return <>
    <input
      value={input}
      onChange={(e) => handleInputChange(e.target.value)}
    />
      {isPending && <p>Search is updating...</p>}
    <SearchResults searchString={searchString} />
  </>
  
}

Listing 2 zeigt dieses Verhalten. Die Komponente hat zwei Zustände: einen für das Eingabefeld (input) und einen für den Suchstring (searchString), der verwendet wird, um eine fiktive Liste zu filtern. Die Annahme ist, dass die Liste sehr groß und das Filtern und Rendern entsprechend lange dauern. Wenn der Text im Eingabefeld aktualisiert wird, wird zunächst, wie gewohnt mit setInput dessen Zustand aktualisiert. Außerdem wird eine Transition gestartet, in der auch der Suchstring gesetzt wird.

Ändert sich die Eingabe, wird React die Komponente wie gewohnt neu rendern. Dabei ist der input State dann aktualisiert (mit der Eingabe des Benutzers), der State für den Suchstring allerdings noch nicht. Stattdessen liefert isPending nun true zurück. Die Komponente kann also das Eingabefeld aktualisieren und die Benutzereingabe wird unmittelbar sichtbar. Da der Suchstring noch auf dem alten Wert steht, muss die Tabelle nicht neu aufgebaut werden – und zeigt entsprechend noch den alten Inhalt. Um den Benutzer darauf aufmerksam zu machen, kann die isPending Information verwendet werden, um einen entsprechenden Hinweis auszugeben. Sofern nach dem ersten Render-Zyklus keine weiteren dringenden State-Updates anstehen, wird React die Komponente ein zweites Mal rendern. Nun liefert auch der State für den Suchstring dessen aktuellen Wert, sodass die Anwendung die neuen Suchergebnisse darstellen kann.

Grundsätzlich kann React dieses Rendern jederzeit wieder unter- oder abbrechen, sobald erneut ein wichtiges Update auftritt. Werden zum Beispiel sehr schnell hintereinander mehrere Zeichen in das Suchfeld eingegeben, zum Beispiel „Meier“, so könnte es sein, dass React die Komponente mehrfach hintereinander mit dem aktualisierten Eingabetext rendert, aber ohne den aktualisierten Suchstring. Abbildung 1 zeigt dieses Verhalten anhand von Logausgaben auf der Konsole, die jedes Mal ausgegeben werden, wenn React die Komponente neugerendert und das Ergebnis in das DOM committet hat. Erst nach dem letzten eingegeben Zeichen rendert React in diesem Beispiel die Komponente mit dem neuen Wert auch für den Suchstring. Während der vorherigen Renderings wird nur der Wert des Eingabefelds neu vom useState Hook zurückgeliefert. Der Text für den Suchstring bleibt jeweils noch auf dem initialen Stand (leerer String) und isPending ist true, da dieser Wert nicht aktuell ist. Dabei ist auch zu sehen, dass React die „verlorenen“ Renderings für den Suchstring nicht nachholt, sondern nur einmal die Komponente mit dem dann aktuellen Suchstring neu rendert.

hartmann_react18_1.tif_fmt1.jpgAbb. 1: Renderings mit unterschiedlicher Priorität

Eingebautes Throttling – useDeferredValue

Das Problem häufiger und teurer Updates wird auch durch den zweiten neuen Hook adressiert, der vom Concurrent Rendering profitiert. Im zuvor gezeigten Beispiel hat die App-Komponente die SearchResults-Komponente entlastet, indem sie die Aktualisierung des Suchstrings als weniger wichtig markiert hat. Dadurch wird die SearchResults-Komponente nicht nach jeder Eingabe neu gerendert.

Eine Komponente kann ein ähnliches Verhalten aber auch selbst implementieren. Dazu kann sie sich vom neuen Hook useDeferredValue als Wert einen verzögerten Wert geben lassen, der von React nicht notwendigerweise bei jedem Rendern aktualisiert wird.

Listing 3: Der useDeferredValue Hook

function SearchResults({ searchString }) {
  // deferredSearchString wird von React nicht bei jedem
  // Rendern aktualisiert. deferredSearchString kann also
  // "älter" als searchString sein
  const deferredSearchString = useDeferredValue(searchString);
 
  return <SearchTable term={deferredSearchString} />;
}
 
const SearchTable = React.memo(function({term}) {
  // hier wird die komplexe Tabelle gerendert
});

Der SearchResults-Komponente in Listing 3 wird bei jedem Rendern ein Suchstring als Property übergeben, dafür erhält sie aber einen deferred-Wert, der nicht zwingen aktuell sein muss. So kann es sein, dass die SearchResults-Komponente nacheinander mit dem searchString „m“„me“„mei“ aufgerufen wird, der deferredSearchString aber z. B. „m“„m“ und dann „mei“ zurückliefert. Durch die React.memo-Funktion würde die Komponente, die schlussendlich die komplexe Tabelle darstellt (SearchTable), nur zweimal neu gerendert werden (für „m“ und für „mei“) und nicht dreimal. Auf diese Weise kann Debouncing bzw. Throttling implementiert werden, ohne dass dabei explizit ein Timeout angegeben werden muss, wann der Wert zu aktualisieren ist. Um den bestmöglichen Zeitpunkt für die Aktualisierung kümmert sich React automatisch.

Zusammenfassen von Renderings

Neben dem Concurrent Rendering gibt es noch eine Verhaltensänderung in React, die das Neurendern optimieren soll. Selbst wenn mehrere State-Updates als Folge einer Benutzerinteraktion in einer Handler-Funktion durchgeführt werden, rendert React die Komponente nur einmal neu. Die verschiedenen Aufrufe der Setter-Funktionen von useState werden zusammengefasst. Dieses Verhalten nennt sich Batching und funktioniert ab React 18 auch für Code, der nicht in Handler-Funktionen untergebracht ist, sondern z. B. in Promises oder Timeouts.

Listing 4 zeigt eine mögliche Verarbeitung der Antwort eines HTTP-Aufrufs mit dem fetch API. Dabei werden zwei Zustände gesetzt, neben den geladenen Daten (setPosts) auch die Information, dass das Laden der Daten abgeschlossen ist (isLoading).

Listing 4: State-Änderungen im Promise-Callback

function Blog() {
  const [posts, setPosts] = useState(null);
  const [loading, isLoading] = useState(true);
 
  useEffect( () => {
    fetch("https://...")
      .then(response => response.json())
      .then(posts => {
        setPosts(loaded);
        setLoading(false);
      });
  });
 
  return ...;
}

In früheren React-Versionen würden daraus zwei Renderzyklen folgen. Ab React 18 wird die Komponente erst nach dem zweiten Setter-Aufruf (setLoading) einmal neu gerendert. Dadurch werden nicht nur unnötige Renderzyklen vermieden, sondern auch Konsistenzprobleme behoben.

Neue Hooks

In React 18 gibt es den neuen Hook useId, mit dem eine eindeutige ID für HTML-Elemente erzeugt werden kann. Das kann zum Beispiel nützlich sein, wenn sich Elemente innerhalb einer Komponente referenzieren müssen, z. B. mit label-for oder aria-Attributen wie aria-describedby. In der Vergangenheit musste eine Komponente entweder eine ID von außen per Property erhalten oder die Komponente musste selbst eine ID generieren, die dann allerdings für die ganze Anwendung eindeutig sein musste. Außerdem musste sichergestellt werden, dass sich die IDs auf dem Server und dem Client identisch verhielten, also dass eine auf dem Server vorgerenderte Anwendung dieselbe ID bekommt wie später beim Rendern auf dem Client. Diese beiden Probleme löst useId. Die zurückgelieferte ID ist eindeutig und auf dem Server und Client identisch. Listing 5 zeigt die Verwendung. Hier wird eine ID generiert, die für das input-Element verwendet wird, damit das label-Element zugewiesen werden kann. Außerdem wird basierend auf der ursprünglichen ID eine zweite ID durch Konkatenation erzeugt, die dann verwendet wird, um die Beschreibung des input-Felds über das ariaDescribedBy-Attribut mit dem p-Element zu verknüpfen.

Listing 5: Verwendung des useId Hook

function Input({label, value, onChange, desc}) {
  const id = useId();
 
  return <>
    <label htmlFor={id}>{label}</label>
    <div>
      <input id={id} 
        ariaDescribedBy={id + '-desc'}
        value={value} 
        onChange={onChange} />
      <p id={id + '-desc'}>{desc}</p>
    </div>
  </>
}

Hooks für Bibliotheken

Zwei weitere Hooks, die in React 18 hinzugekommen sind, sind nicht für die Verwendung direkt in einer Anwendung gedacht, sondern für die Verwendung in Bibliotheken. Einer davon ist useSyncExternal-Store, mit dem State-Management-Bibliotheken in einer Komponente sich an ihrem externen Store registrieren können. Dieser Hook ist notwendig geworden, damit Bibliotheken wie Redux auch mit dem geänderten Rendering-Verhalten durch das Concurrent Rendering korrekt funktionieren. Der Hook useInsertionEffect ist in erster Linie für CSS-in-JS-Styling-Bibliotheken wie styled-components relevant und erlaubt es ihnen, CSS-Änderungen rechtzeitig vor der Aktualisierung der Darstellung im Browser durchzuführen.

Ausblick

Das wichtigste Feature von React 18 ist sicherlich das Concurrent Rendering, auch wenn sich das im API für Anwendungen aktuell in „nur“ zwei neuen Hooks, useTransition und useDeferredValue, niederschlägt. Die meiste Arbeit macht React transparent im Hintergrund. Auf Basis des Concurrent Rendering soll aber in nächster Zeit noch eine ganze Reihe von neuen Features hinzukommen, die die Arbeit mit React entscheidend verändern dürften – und trotzdem als Minor-Versionen geplant sind, also weiterhin ohne bestehende APIs zu verändern oder bestehende Anwendungen zu beeinträchtigen.

Vorrendern von Komponenten

Eine Idee, die schon lange gehandelt wird, sind beispielsweise Komponenten, die vorgerendert werden, um sie bei Bedarf sehr schnell darstellen zu können. Das könnte zum Beispiel in einer Tabkomponente der Fall sein. Der aktuelle Tabreiter samt Inhalt ist sichtbar. Der nächste Tabreiter könnte schon im Verborgenen vorgerendert werden, sodass er beim Anklicken des Tabs sehr schnell zur Verfügung steht und sichtbar wird. Auch könnte der vorherige Tabreiter aufbewahrt werden, um ihn beim erneuten Auswählen schnell darstellen zu können. Dieses Feature wird möglicherweise mit einer neuen Off-Screen-Komponente umgesetzt werden.

Suspense for Data Loading

Auch das Suspense for Data Loading wird nun wohl (endlich) umgesetzt werden. Suspense gibt es schon längere Zeit in React. Dabei handelt sich um eine Möglichkeit, mit der eine Komponente deklarativ anzeigen kann, dass eine Fallback-Komponente dargestellt werden soll, solange der darunterliegende Komponentenbaum nicht vollständig gerendert werden kann. Das funktioniert aktuell mit Komponenten, die lazy geladen werden: Wird eine solche Komponente das erste Mal benötigt, muss ihr JavaScript-Code erst vom Server geladen und ausgeführt werden. In dieser Zeit wird eine Fallback-Komponente dargestellt. Erst wenn die benötigte Komponente geladen und vollständig gerendert wurde, wird die Fallback-Komponente entfernt und durch den gerenderten Komponentenbaum ersetzt.

Listing 6: Suspense mit Lazy Loading

import React, { Suspense } from 'react';
import { Switch, Route } from 'react-router-dom';
 
 
const Profile = React.lazy(() => import('./Profile'));
const BlogList = React.lazy(() => import('./BlogList'));
 
function App() {
  return (
    <Switch>
      <Suspense fallback={<h1>Loading...</h1>}>
        <Route path="/profile">
          <Profile />
        </Route>
        <Route path="/blog">
          <BlogList />
        </Route>
      </Suspense>
    </Switch>
  );
}

Listing 6 zeigt ein typisches Beispiel. Hier werden zwei Komponenten, Profile und BlogList, dynamisch importiert, also erst bei Bedarf vom Server geladen. Der React Router steuert, welche Komponente sichtbar sein soll. Beim ersten Rendern der jeweiligen Komponente wird die fallback-Komponente von Suspense angezeigt (Loading…), bis der JavaScript-Code für die Komponente geladen wurde.

In welcher Form dieses Verhalten für das Laden von Daten zur Verfügung gestellt wird, ist noch unklar. Erwartet wird, dass die entsprechenden APIs möglicherweise nicht direkt in der Anwendung verwendet, sondern indirekt über Bibliotheken verwendet werden, zum Beispiel über den React Router oder Redux. Erste Beispiele bzw. Überlegungen dazu gibt es in diesen beiden Bibliotheken bereits.

Das Beispiel in Listing 7 zeigt die Verwendung der Bibliothek React Query, die ebenfalls bereits experimentelle Unterstützung für Suspense bietet.

Listing 7: React Query mit Suspense for Data Loading

import { Suspense, useTransition } from "react";
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from "react-query";
 
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
    },
  },
});
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<h1>Waiting for Data...</h1>}>
        <BlogList />
      </Suspense>
    </QueryClientProvider>
  );
}
 
function BlogList() {
  const { data } = useQuery<BlogPost[]>("posts", () =>
    fetch("http://localhost:7000/posts?slow")
    .then((response) => response.json()
    )
  );
 
  return (
    <>
      {data.map((p) => (
        <article key={p.id}>
          <p className="Date">{p.date}</p>
          <h1>
            {p.title}
          </h1>
          <p>{p.body}</p>
        </article>
      ))}
      </>
  );
}

Wenn die App-Komponente gerendert wird, wird React versuchen, die BlogList-Komponente zu rendern. Da der useQuery Hook von React Query intern Suspense verwendet, kann dieser Hook React signalisieren, dass er noch auf Daten wartet (das Ergebnis des fetch-Aufrufs). So lange zeigt React dann die Fallback-Komponente an, die mit der nächsthöheren Suspense-Komponente festgelegt ist (in diesem Fall ein h1-Element mit einer einfachen Wartemeldung). Sobald der useQuery Hook die Daten ermittelt hat, wird React die Anwendung neu rendern. Nun stehen der BlogList-Komponente die Daten zur Verfügung und sie kann gerendert werden. Übrigens würde hier auch der Fehlerfall nicht mehr innerhalb der Komponente behandelt werden, sondern durch eine Error-Boundary-Komponente, die darum herum gelegt wird.

Die Einführung von Suspense beim Laden von Daten wird die Entwicklung von React-Anwendungen sicherlich massiv beeinflussen. Bessere und flüssigere Übergänge von einer Ansicht zur nächsten (bei der Daten geladen werden müssen) wären die Folge. Mittels des deklarativen API kann die Anwendung dabei selbst bestimmen, an welcher Stelle das Rendern unterbrochen wird, wenn noch irgendwo in der Komponentenhierarchie Daten benötigt werden.

Listing 8 zeigt ein weiteres Beispiel. Hier verwendet die SinglePostPage-Komponente zwei weitere Komponenten, um einen BlogPost und dessen Kommentare zu lesen und darzustellen. Die beiden Komponenten, die dafür eingesetzt werden, verwenden jeweils useQuery, um ihre Daten zu laden. Je nachdem, wo die SinglePostPage-Komponente (oder sogar der Verwender der Komponente) die Suspense-Komponenten einzieht, verändert sich das Verhalten.

Listing 8: Suspense-Komponente mit zwei Unterkomponenten, die auf Daten warten

function SinglePostPage({ postId }) {
 
  return (
    <div>
      <h1>Post App</h1>
        <Suspense fallback={<h1>Loading Blog Post...</h1>}
      <SinglePost postId={postId} />
      <Comments postId={postId} />
  </Suspense>
    </div>
  );
}
 
function SinglePost({ postId }) {
  const { data } = useQuery(["post", postId], 
    () => /* fetch ... */ );
 
  return ...; // geladenen Post darstellen
}
 
function Comments({ postId }: { postId: string }) {
  const { data } = useQuery(["comments", postId]
, () => /* fetch ... */ );
 
  return ...; // geladene Kommentare darstellen
}

In der Konfiguration, die in Listing 8 zu sehen ist, würde die Fallback-Komponente („Loading Blog Post…“) so lange angezeigt werden, bis sowohl der Request für den Post als auch für die Kommentare abgeschlossen ist. Die Komponente könnte aber auch einen eigenen Suspense-Block für beide Komponenten einzeln definieren, es würden entsprechend zwei Fallback-Komponenten dargestellt. Aber sobald eine Komponente ihre Daten hat, würde diese Komponente auch schon gerendert werden.

Alternativ kann die Komponente auch ganz auf Suspense-Komponenten verzichten. In diesem Fall wäre eine Oberkomponente dafür verantwortlich, eine Suspense-Komponente einzubauen.

Server Components

Pünktlich zu Weihnachten 2020 wurde etwas überraschend ein Konzept für Server Components vorgestellt. Dabei handelt es sich um normale React-Komponenten, die allerdings ausschließlich auf dem Server ausgeführt werden und direkt auf die Serverinfrastruktur zugreifen können, zum Beispiel auf das Dateisystem oder Datenbanken. Die Komponenten werden dann auf dem Server gerendert und die gerenderte Komponente an den Client übertragen. Dadurch, dass die Komponenten nur auf dem Server gerendert werden, entfällt die Notwendigkeit, den JavaScript-Code für die Komponente auf den Client übertragen zu müssen. Das soll Bandbreite und Performance sparen und bietet sich insbesondere für Anwendungen an, die viel statischen Content anzeigen. Und hierin unterscheiden sich die Server Components auch vom serverseitigen Rendern (SSR): Bei SSR werden die Komponenten zwar auch auf dem Server ausgeführt und in gerenderter Form initial auf den Client übertragen, zusätzlich wird aber auch der JavaScript-Code der Komponenten und diese auch dort erneut gerendert, um sie interaktiv zu machen.

Die Server Components sind zwar grundsätzlich von Concurrent React unabhängig, machen allerdings starken Gebrauch des Suspense-Features und des Streaming-Supports. Ein mögliches Verhalten könnte zum Beispiel sein, dass eine Anwendung auf dem Server gerendert wird, eine Serverkomponente allerdings noch auf Daten wartet. Mit einer Suspense-Komponente kann – wie auf dem Client – ein Punkt definiert werden, an dem eine Fallback-Komponente angezeigt wird. Diese würde nun auch schon an den Client gesendet und dort angezeigt werden. Sobald auf dem Server die Daten geladen worden sind, werden die noch fehlenden Komponenten nachträglich gerendert und ebenfalls auf den Client übertragen – aber natürlich nicht die ganze Anwendung erneut, sondern nur die Teile, die sich seit dem Rendern der Fallback-Komponente verändert haben.

Hierfür muss dann auch eine Serverumgebung mit dem entsprechenden Build-Prozess, Deployment etc. zur Verfügung stehen. Erste Verwendung bzw. Unterstützung für diese Komponenten wird deshalb für Frameworks wie Next.js erwartet. Dieses Framework bietet ab Version 12 bereits experimentelle Unterstützung für die Server Components. Dadurch wird das API der Next.js-Komponenten vereinfacht, da Methoden wie getServerSideProps bzw. getStaticProps entfallen. Die Logik aus diesen Funktionen kann nun direkt in der React-Komponente erfolgen.

Zusammenfassung

Mit dem Concurrent Rendering in React 18 steht nach langer Zeit ein Feature zur Verfügung, das lange angekündigt und immer wieder verschoben wurde. Damit legt React die Grundlagen für weitere große Veränderungen, denn insbesondere mit Suspense und Server Components, die nun in Reichweite sind, wird sich die Art und Weise, wie React-Anwendungen gebaut werden, möglicherweise noch einmal verändern. Das Ergebnis soll Code sein, der einfacher zu entwickeln ist, gleichzeitig aber noch flüssigere UIs erzeugt. Durch die Erweiterungen sowohl beim serverseitigen Rendern mit der Streaming-Unterstützung als auch mit den Server Components erfährt auch der serverseitige Einsatz von React spannende Neuerungen.

Immer auf dem Laufenden bleiben!
Alle News & Updates: