React-Deep-Dive: TanStack Router und TanStack Query – Teil 2

TanStack Router und TanStack Query bringen frischen Wind in React-Anwendungen. Beispiele zeigen ihren Einsatz fĂĽr Darstellung, Datenspeicherung und mehr.

vorlesen Druckansicht
JavaScript-Bibliothek React

(Bild: erzeugt mit KI durch iX)

Lesezeit: 20 Min.
Von
  • Nils Hartmann
Inhaltsverzeichnis
close notice

This article is also available in English. It was translated with technical assistance and editorially reviewed before publication.

Der erste Teil der Artikelserie hat die Grundlagen von TanStack Router und Query gezeigt. In diesem zweiten und letzten Teil geht es um darauf aufbauende Features, die Anwendungen nutzen können, um ihre Darstellung zu optimieren, Daten zu speichern und die URL zum Speichern globaler Informationen zu verwenden.

Nils Hartmann
Nils Hartmann

Nils Hartmann ist freiberuflicher Softwareentwickler und Coach mit den Schwerpunkten Java im Backend und React im Frontend, wozu er auch Workshops und Trainings gibt.

React-Deep-Dive: TanStack Router und TanStack Query

Die Detailansicht eines Tasks zeichnet sich durch zwei technische Besonderheiten aus. Zum einen enthält der Pfad zu der Route einen variablen Platzhalter, der zur Laufzeit mit einer Task-Id befüllt werden muss (/task/1, /task/2 etc.). Zum anderen müssen zur vollständigen Darstellung des Tasks Daten aus zwei verschiedenen Endpunkten gelesen werden.

Detailansicht eines Tasks mit Ergänzungen der User ("Insights")

(Bild: Nils Hartmann)

Zunächst zu der Route für die Detaildarstellung: Diese enthält einen Platzhalter ("taskId"), da die Id des darzustellenden Tasks nicht zur Entwicklungszeit, sondern erst zur Laufzeit beim Aufrufen der Route bekannt ist. Der Name der Routendatei muss aus diesem Grund mit einem $-Zeichen beginnen, zum Beispiel tasks/$taskId.tsx. Die generierten TypeScript-Typen für diese Route stellen sicher, dass beim Erstellen eines Links zu dieser Route der Wert für den Platzhalter korrekt angegeben wird. Dasselbe gilt für den Zugriff auf den Wert aus der URL zur Laufzeit.

Das Laden der Daten für die Detaildarstellung kann grundsätzlich genauso erfolgen wie das Laden der Liste mit den Tasks. Allerdings soll hier exemplarisch das Suspense-Feature von React demonstriert werden, mit dem Zusammenspiel von TanStack Router und TanStack Query.

Um mit dem Laden der Daten für eine Route so frühzeitig wie möglich zu beginnen, können die Daten bereits in der loader-Methode einer Route geladen werden. Diese Methode lässt sich in der Routenkonfiguration angeben. Ist sie gesetzt, ruft der Router sie bereits vor dem Rendern der Route auf, um Daten für die Route zu ermitteln. In welcher Form sie das tut, und ob sie synchron oder asynchron arbeitet, bleibt der Methode überlassen. Komponenten können auf die Daten dann über den useLoaderData-Hook zugreifen.

Allerdings ist innerhalb der loader-Methode die Verwendung von Hook-Funktionen wie useQuery unzulässig. Deshalb arbeitet man hier direkt mit dem QueryClient. Da dieser ein globales Objekt ist, lässt er sich aus einem Modul exportieren und in den jeweiligen Routendateien importieren. Alternativ bietet der Router einen eigenen Context, der beim Starten der Anwendung mit beliebigen Daten, beispielsweise dem QueryClient, befüllbar ist. Der Router übergibt das Context-Objekt sowie ein Objekt mit allen Pfadparametern (in diesem Fall bestehend aus "taskId") an die Loader-Funktion.

Videos by heise

Das Listing zeigt eine "Hello World"-Implementierung der loader-Methode für die $taskId-Route, die das grundsätzliche Verhalten demonstriert. Die loader-Methode verwendet den Context zum Zugriff auf das QueryClient-Objekt und lädt darüber die Daten für den Task mit der taskId aus der URL. Ähnlich wie useQuery liefert ensureQueryData die angeforderten Daten aus dem Cache zurück. Sind sie dort nicht vorhanden, ruft TanStack Query die angegebene Query-Funktion auf.

// /src/routes/tasks/$taskId.tsx
export const Route = createFileRoute("/tasks/$taskId")({
  component: RouteComponent,
  pendingComponent: () => <H3>Loading Task Details...</H3>,
  async loader({ context, params }) {
   const taskId = params.taskId;
   return context.queryClient.ensureQueryData({
     queryKey: [“tasks”, “details”, taskId ],
     queryFn() { return fetch(“...”)....; }
   });
  }
});

function RouteComponent() {
  
  const task = Route.useLoaderData();
  
  return <TaskDetails task={task} />
}

Die Routenkomponente greift auf die gelesenen Daten zu, um den Task darzustellen. Der TypeScript-Typ fĂĽr den RĂĽckgabewert ergibt sich automatisch aus dem RĂĽckgabewert der loader-Methode, sodass auch hier die korrekte Verwendung der Daten sichergestellt ist.

In diesem Beispiel wartet der Router mit dem Rendern der RouteComponent bis zur Auflösung des von loader zurückgegebenen Promise. Durch die Komponente "pendingComponent" erscheint visuelles Feedback während der Wartezeit.

In der Beispielanwendung benötigt die Route allerdings nicht eine Query, sondern zwei. Denn zu jedem Task gibt es noch sogenannte Insights. Das sind Ergänzungen oder Hinweise, die andere Benutzer an einen Task hängen können. Die Insights stellt das Backend über einen separaten Endpunkt zur Verfügung, sodass das Frontend bei der Task-Darstellung nun auch auf diesen Endpunkt zugreifen muss. Grundsätzlich ist es kein Problem, die loader-Methode anzupassen, und dort auch einen zweiten Request durchzuführen. Der Router würde dann auf das Ergebnis beider Requests warten, bevor er die Routenkomponente rendert. Allerdings soll das Rendern bereits geschehen, sobald die Daten für die Task-Details vorliegen – denn diese sind die wichtigsten Informationen der Seite, die schnellstmöglich erscheinen sollen und auch ohne die Insights bereits einen Mehrwert für Anwenderinnen und Anwender bedeuten.

Dauert der Request für die Insights länger als das Lesen der Task-Details, soll an der Stelle in der Komponente bis zum Laden der Insights ein Platzhalter ausgegeben werden. Umgekehrt sollen allerdings die Insights in keinem Fall vor den Task-Details erscheinen, da dies verwirren würde. (Diese Form von Priorisierung ließe sich zum Beispiel auch in einer Shopping-Anwendung einsetzen, wenn die Darstellung eines Produkts bereits erfolgen soll, bevor auch die Bewertungen zu dem Produkt vorliegen.)

Um mit dem (potenziell langsamen) Laden der Daten beider Endpunkte jeweils so früh wie möglich anzufangen, verwendet die Loader-Funktion zwei ensureQueryData-Aufrufe, wartet aber nicht auf deren Ergebnisse und gibt auch nichts zurück. Aus Sicht des Routers kann dieser also unmittelbar mit dem Rendern der Komponente beginnen. Das nächste Listing zeigt die überarbeitete Loader-Funktion und die Routenkomponente, die nun die Task-Details und die Insights rendert.

// /src/routes/tasks/$taskId.tsx

import { Suspense } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
// ...

export const Route = createFileRoute("/tasks/$taskId")({
  component: RouteComponent,
  pendingComponent: () => <H3>Loading Task</H3>,
  loader({ context, params }) {
    const taskId = params.taskId;
    // Starten der beiden Requests parallel, ohne auf das 
    // Ergebnis zu warten
    context.queryClient.ensureQueryData({
      queryKey: ["tasks", "details", taskId], 
      queryFn() { /* ... */ },
    });
    context.queryClient.ensureQueryData({
      queryKey: ["tasks", "details", taskId, "insights"], 
      queryFn(): { /* ... */ }
    }
});

function RouteComponent() {
  const taskId = Route.useParams();

  return (
    <>
      <TaskDetails taskId={taskId} />
      <Suspense fallback={<H3>Loading Insights...</H3>}>
        <InsightList taskId={taskId} />
      </Suspense>
    </>
  );
}

function TaskDetails({ taskId }) {
  const task = useSuspenseQuery({
    queryKey: ["tasks", "details", taskId], 
    queryFn() { /* ... */ },
  });

  return  /* Task-Daten darstellen */;
}

function InsightList({ taskId }) {
  const insights = useSuspenseQuery({
    queryKey: ["tasks", "details", taskId, "insights"], 
    queryFn() { /* ... */ },
  });


  return  /* Insight-Daten darstellen */
}

Die TaskDetails-Komponente verwendet useSuspenseQuery, um die Daten aus dem Request beziehungsweise aus dem Cache abzufragen. Da sowohl die Loader-Funktion als auch die TaskDetails-Komponente denselben Query Key verwenden, stellt TanStack Query sicher, dass nur ein Server Request läuft beziehungsweise die Daten aus dem Cache gegeben werden, sofern sie unter dem Key dort schon vorhanden sind. Die TaskDetails-Komponente kann in dem Fall vollständig zu Ende gerendert werden.

Sind die Daten aber noch nicht vorhanden, unterbricht React das Rendern der Komponente an dieser Stelle und sucht stattdessen in der Komponentenhierarchie oberhalb nach einer sogenannten Suspense Boundary. Diese Komponente kann einen Platzhalter ausgeben, solange das Rendern des Komponentenbaums unterbrochen ist. Dieses Verhalten ist in etwa vergleichbar mit einem Fehler, den eine Funktion wirft: Der Fehler wird im Callstack so weit nach oben gereicht, bis ein try...catch-Handler gefunden wird. Im Fall von React wird nicht nach einem Try-Catch-Handler gesucht, sondern nach der React-Suspense-Komponente. Die Routenkomponente, die die TaskDetails-Komponente rendert, verwendet aber keine Suspense-Komponente. Die nächsthöhere Suspense-Komponente in der Komponentenhierarchie stellt der Router bereit. Sie sorgt für das Anzeigen der pendingComponent aus der Routenkonfiguration als Fallback-Komponente. Sobald der noch laufende Server Request abgeschlossen ist und der useSuspenseQuery-Aufruf die ausstehenden Daten erhält, rendert React die TaskDetails-Komponente automatisch neu und entfernt die Fallback-Komponente aus der Komponentenhierarchie, sodass nun die TaskDetails-Komponente erscheint.

Die Routenkomponente rendert außerdem die InsightsList-Komponente, die ebenfalls einen useSuspenseQuery-Aufruf durchführt. Hier gilt das gleiche Prinzip: Wenn die Daten noch nicht vorhanden sind, unterbricht React auch hier das Rendern und sucht nach der nächsthöheren Suspense-Komponente. In diesem Fall wird React in der Routenkomponente fündig, denn die InsightList-Komponente ist mit einer solchen Komponente umschlossen. Hier wird also nur die InsightList-Komponente durch die angegebene Fallback-Komponente ersetzt, solange die Insight-Daten noch nicht vorliegen. Die parallel liegende TaskDetails-Komponente bleibt dennoch sichtbar. Mit den Suspense-Komponenten lassen sich einzelne Teile der Oberfläche also präzise priorisieren, da Entwicklerinnen und Entwickler festlegen können, an welchen Stellen das Rendern pausieren soll, sofern noch Daten fehlen.

Zurück zur Loader-Funktion: In der gezeigten Implementierung startet sie die beiden Requests parallel, um die Wartezeit so gering wie möglich zu halten. Ohne die Loader-Funktion würden die Requests erst beim Rendern von TaskDetails beziehungsweise InsightList starten. Im Fall der Komponente TaskDetails wäre das womöglich zu verschmerzen, denn ihr Rendern erfolgt fast sofort nach der Loader-Funktion. Allerdings würde die Insights-Query in dem Fall erst ausgeführt werden, wenn die TaskDetails dargestellt werden konnten, da bis dahin die pendingComponent angezeigt wird. Es käme hier zum sequenziellen Ausführen von Requests ("Request Waterfalls") und damit zu einer langsameren Anwendung beziehungsweise einer unnötig verspäteten Darstellung der InsightList-Komponente.

Ein weiterer Grund spricht dafür, das Laden der Daten bereits in der Loader-Funktion zu beginnen. Der Router ist in der Lage, die Loader-Funktionen schon vor dem Aktivieren der Route auszuführen. Dazu lässt sich mit der Link-Komponente eine Preload-Strategie festlegen. Diese definiert, ob und wann die Daten einer Route im Voraus zu laden sind. Mögliche Werte sind zum Beispiel "intent" oder "viewport". Im ersten Fall läuft der Loader einer Route bereits dann, wenn sich der Maus-Cursor über dem Link befindet, sodass der Router davon ausgeht, dass die Anwenderin oder der Anwender wahrscheinlich auf den Link klicken wird. Ein Link, der die Preload-Strategie "viewport" gesetzt hat, führt dazu, dass die Daten vorgeladen werden, sobald der Link im Viewport des Browsers sichtbar wird. Das Listing zeigt die Link-Komponente, die einen Link auf die Detail-Darstellung eines Tasks zeigt und dafür sorgt, dass die Loader-Funktion bereits ausgeführt wird, wenn der Cursor über dem Link steht:

function TaskTableRow({ task }) {
  return <tr>
    <td>
      <Link
        to={"/tasks/$taskId"}
        params={{ taskId: task.id }}
        preload={"intent"}
      >
        {task.title}
      </Link>
    </td>
    { /* ... */ }
  </tr>
}
JavaScript-Konferenz von Heise: enterJS 2025
Enterprise-JavaScript-Konferenz enterJS 2025, 7. und 8. Mai in Mannheim

(Bild: WD Ashari/Shutterstock.com)

Die enterJS 2025 findet am 7. und 8. Mai in Mannheim statt. Die Konferenz bietet einen umfassenden Blick auf die JavaScript-gestĂĽtzte Enterprise-Welt. Der Fokus liegt nicht nur auf den Programmiersprachen JavaScript und TypeScript selbst, sondern auch auf Frameworks und Tools, Accessibility, Praxisberichten, UI/UX und Security.

Highlights aus dem Programm:

Tickets sind zum Frühbucherpreis im Online-Shop erhältlich.