Framework
Version

Erweitertes Server-Rendering

Willkommen zum Leitfaden für fortgeschrittenes Server-Rendering, in dem Sie alles über die Verwendung von React Query mit Streaming, Server-Komponenten und dem Next.js App Router erfahren.

Sie möchten vielleicht zuerst den Leitfaden für Server-Rendering & Hydration lesen, da er die Grundlagen für die Verwendung von React Query mit SSR vermittelt, und auch Performance & Request Waterfalls sowie Prefetching & Router Integration enthalten wertvolle Hintergrundinformationen.

Bevor wir beginnen, möchten wir darauf hinweisen, dass der im SSR-Leitfaden beschriebene Ansatz mit initialData auch mit Server-Komponenten funktioniert. Wir werden uns in diesem Leitfaden auf die Hydration-APIs konzentrieren.

Server-Komponenten & Next.js App Router

Wir werden hier nicht ausführlich auf Server-Komponenten eingehen, aber kurz gesagt handelt es sich um Komponenten, die garantiert *nur* auf dem Server ausgeführt werden, sowohl für die anfängliche Seitenansicht als auch **bei Seitenübergängen**. Dies ähnelt der Funktionsweise von Next.js getServerSideProps/getStaticProps und Remix loader, da diese auch immer auf dem Server ausgeführt werden. Während diese jedoch nur Daten zurückgeben können, können Server-Komponenten viel mehr tun. Der Datenanteil ist jedoch zentral für React Query, also konzentrieren wir uns darauf.

Wie können wir das, was wir im Leitfaden für Server-Rendering über das Übergeben von Daten, die in Framework-Loadern vorab abgerufen wurden, an die App gelernt haben, auf Server-Komponenten und den Next.js App Router anwenden? Der beste Weg, darüber nachzudenken, ist, Server-Komponenten als "nur" einen weiteren Framework-Loader zu betrachten.

Eine kurze Anmerkung zur Terminologie

Bisher haben wir in diesen Leitfäden über den *Server* und den *Client* gesprochen. Es ist wichtig zu beachten, dass dies verwirrenderweise nicht 1:1 mit *Server-Komponenten* und *Client-Komponenten* übereinstimmt. Server-Komponenten werden garantiert nur auf dem Server ausgeführt, aber Client-Komponenten können tatsächlich an beiden Orten ausgeführt werden. Der Grund dafür ist, dass sie auch während des anfänglichen *Server-Rendering*-Durchlaufs gerendert werden können.

Eine Möglichkeit, dies zu betrachten, ist, dass Server-Komponenten zwar auch *rendern*, dies aber während einer "Loader-Phase" geschieht (immer auf dem Server), während Client-Komponenten während der "Anwendungsphase" ausgeführt werden. Diese Anwendung kann sowohl auf dem Server während SSR als auch z. B. in einem Browser ausgeführt werden. Wo genau diese Anwendung ausgeführt wird und ob sie während SSR ausgeführt wird, kann je nach Framework variieren.

Ersteinrichtung

Der erste Schritt jeder React Query-Einrichtung ist immer die Erstellung eines queryClient und das Umschließen Ihrer Anwendung mit einem QueryClientProvider. Bei Server-Komponenten sieht dies über alle Frameworks hinweg weitgehend gleich aus, ein Unterschied sind die Dateinamenskonventionen

tsx
// In Next.js, this file would be called: app/providers.tsx
'use client'

// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export default function Providers({ children }: { children: React.ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )
}
// In Next.js, this file would be called: app/providers.tsx
'use client'

// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export default function Providers({ children }: { children: React.ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )
}
tsx
// In Next.js, this file would be called: app/layout.tsx
import Providers from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head />
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
// In Next.js, this file would be called: app/layout.tsx
import Providers from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head />
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Dieser Teil ist dem, was wir im SSR-Leitfaden gemacht haben, ziemlich ähnlich. Wir müssen die Dinge nur in zwei verschiedene Dateien aufteilen.

Daten vorab abrufen und de-/hydrieren

Als nächstes sehen wir uns an, wie man Daten tatsächlich vorab abruft, dann dehydriert und hydriert. So sah es mit dem **Next.js Pages Router** aus

tsx
// pages/posts.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
  useQuery,
} from '@tanstack/react-query'

// This could also be getServerSideProps
export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the <PostsRoute>, data will be available immediately either way
  //
  // Note that we are using useQuery here instead of useSuspenseQuery.
  // Because this data has already been prefetched, there is no need to
  // ever suspend in the component itself. If we forget or remove the
  // prefetch, this will instead fetch the data on the client, while
  // using useSuspenseQuery would have had worse side effects.
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}

export default function PostsRoute({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Posts />
    </HydrationBoundary>
  )
}
// pages/posts.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
  useQuery,
} from '@tanstack/react-query'

// This could also be getServerSideProps
export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the <PostsRoute>, data will be available immediately either way
  //
  // Note that we are using useQuery here instead of useSuspenseQuery.
  // Because this data has already been prefetched, there is no need to
  // ever suspend in the component itself. If we forget or remove the
  // prefetch, this will instead fetch the data on the client, while
  // using useSuspenseQuery would have had worse side effects.
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}

export default function PostsRoute({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <Posts />
    </HydrationBoundary>
  )
}

Die Umwandlung in den App Router sieht tatsächlich ziemlich ähnlich aus, wir müssen nur die Dinge etwas verschieben. Zuerst erstellen wir eine Server-Komponente, um den Vorabruf-Teil zu erledigen

tsx
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    // Neat! Serialization is now as easy as passing props.
    // HydrationBoundary is a Client Component, so hydration will happen there.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    // Neat! Serialization is now as easy as passing props.
    // HydrationBoundary is a Client Component, so hydration will happen there.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}

Als nächstes sehen wir uns an, wie der Client-Komponenten-Teil aussieht

tsx
// app/posts/posts.tsx
'use client'

export default function Posts() {
  // This useQuery could just as well happen in some deeper
  // child to <Posts>, data will be available immediately either way
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix.
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}
// app/posts/posts.tsx
'use client'

export default function Posts() {
  // This useQuery could just as well happen in some deeper
  // child to <Posts>, data will be available immediately either way
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix.
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}

Eine nette Sache an den obigen Beispielen ist, dass das Einzige, was hier Next.js-spezifisch ist, die Dateinamen sind. Alles andere würde in jedem anderen Framework, das Server-Komponenten unterstützt, gleich aussehen.

Im SSR-Leitfaden haben wir erwähnt, dass Sie den Boilerplate-Code des <HydrationBoundary> in jeder Route entfernen können. Dies ist mit Server-Komponenten nicht möglich.

HINWEIS: Wenn Sie beim Verwenden von asynchronen Server-Komponenten mit TypeScript-Versionen unter 5.1.3 und @types/react-Versionen unter 18.2.8 einen Typfehler erhalten, wird empfohlen, beide auf die neuesten Versionen zu aktualisieren. Alternativ können Sie den temporären Workaround verwenden, indem Sie {/* @ts-expect-error Server Component */} hinzufügen, wenn Sie diese Komponente in einer anderen aufrufen. Weitere Informationen finden Sie unter Async Server Component TypeScript Error in den Next.js 13 Docs.

HINWEIS: Wenn Sie einen Fehler erhalten Nur reine Objekte und einige eingebaute Typen können an Server Actions übergeben werden. Klassen oder Null-Prototypen werden nicht unterstützt. stellen Sie sicher, dass Sie keine Funktionsreferenz an queryFn übergeben, sondern die Funktion aufrufen, da queryFn Argumente eine Reihe von Eigenschaften haben und nicht alle davon serialisierbar sind. Siehe Server Action funktioniert nur, wenn queryFn keine Referenz ist.

Verschachtelte Server-Komponenten

Eine schöne Sache an Server-Komponenten ist, dass sie verschachtelt sein und auf vielen Ebenen im React-Baum existieren können, wodurch es möglich wird, Daten näher dort vorab abzurufen, wo sie tatsächlich verwendet werden, anstatt nur am oberen Ende der Anwendung (genau wie Remix-Loader). Dies kann so einfach sein, dass eine Server-Komponente eine andere Server-Komponente rendert (wir lassen die Client-Komponenten in diesem Beispiel aus Gründen der Kürze weg)

tsx
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'
import CommentsServerComponent from './comments-server'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
      <CommentsServerComponent />
    </HydrationBoundary>
  )
}

// app/posts/comments-server.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Comments from './comments'

export default async function CommentsServerComponent() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Comments />
    </HydrationBoundary>
  )
}
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'
import CommentsServerComponent from './comments-server'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
      <CommentsServerComponent />
    </HydrationBoundary>
  )
}

// app/posts/comments-server.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Comments from './comments'

export default async function CommentsServerComponent() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Comments />
    </HydrationBoundary>
  )
}

Wie Sie sehen können, ist es völlig in Ordnung, <HydrationBoundary> an mehreren Stellen zu verwenden und mehrere queryClient zum Vorabrufen zu erstellen und zu dehydrieren.

Beachten Sie, dass wir, da wir getPosts abwarten, bevor wir CommentsServerComponent rendern, dies zu einem serverseitigen Wasserfall führen würde

1. |> getPosts()
2.   |> getComments()
1. |> getPosts()
2.   |> getComments()

Wenn die Server-Latenz zu den Daten gering ist, ist dies möglicherweise kein großes Problem, aber es ist erwähnenswert.

In Next.js können Sie zusätzlich zum Vorabrufen von Daten in page.tsx auch Daten in layout.tsx und in parallelen Routen vorabrufen. Da diese alle Teil des Routings sind, weiß Next.js, wie sie alle parallel abzurufen sind. Wenn also CommentsServerComponent oben als parallele Route ausgedrückt worden wäre, wäre der Wasserfall automatisch abgeflacht worden.

Wenn mehr Frameworks Server-Komponenten unterstützen, haben sie möglicherweise andere Routing-Konventionen. Lesen Sie die Dokumentation Ihres Frameworks für Details.

Alternative: Eine einzelne queryClient für das Vorabrufen verwenden

Im obigen Beispiel erstellen wir für jede Server-Komponente, die Daten abruft, einen neuen queryClient. Dies ist der empfohlene Ansatz, aber wenn Sie möchten, können Sie alternativ einen einzelnen erstellen, der über alle Server-Komponenten hinweg wiederverwendet wird

tsx
// app/getQueryClient.tsx
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'

// cache() is scoped per request, so we don't leak data between requests
const getQueryClient = cache(() => new QueryClient())
export default getQueryClient
// app/getQueryClient.tsx
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'

// cache() is scoped per request, so we don't leak data between requests
const getQueryClient = cache(() => new QueryClient())
export default getQueryClient

Der Vorteil dabei ist, dass Sie getQueryClient() aufrufen können, um diesen Client überall zu erhalten, wo er von einer Server-Komponente aufgerufen wird, einschließlich Utility-Funktionen. Der Nachteil ist, dass jedes Mal, wenn Sie dehydrate(getQueryClient()) aufrufen, Sie den *gesamten* queryClient serialisieren, einschließlich Abfragen, die bereits zuvor serialisiert wurden und mit der aktuellen Server-Komponente nichts zu tun haben, was unnötiger Overhead ist.

Next.js dedupliziert bereits Anfragen, die fetch() verwenden, aber wenn Sie etwas anderes in Ihrer queryFn verwenden oder wenn Sie ein Framework verwenden, das diese Anfragen *nicht* automatisch dedupliziert, kann die Verwendung eines einzelnen queryClient wie oben beschrieben sinnvoll sein, trotz der duplizierten Serialisierung.

Als zukünftige Verbesserung könnten wir eine dehydrateNew()-Funktion (Name noch nicht festgelegt) in Betracht ziehen, die nur Abfragen dehydriert, die seit dem letzten Aufruf von dehydrateNew() *neu* sind. Fühlen Sie sich frei, uns zu kontaktieren, wenn dies interessant klingt und Sie dazu beitragen möchten!

Datenbesitz und Revalidierung

Mit Server-Komponenten ist es wichtig, über Datenbesitz und Revalidierung nachzudenken. Um zu erklären, warum, schauen wir uns ein modifiziertes Beispiel von oben an

tsx
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  // Note we are now using fetchQuery()
  const posts = await queryClient.fetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {/* This is the new part */}
      <div>Nr of posts: {posts.length}</div>
      <Posts />
    </HydrationBoundary>
  )
}
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  // Note we are now using fetchQuery()
  const posts = await queryClient.fetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      {/* This is the new part */}
      <div>Nr of posts: {posts.length}</div>
      <Posts />
    </HydrationBoundary>
  )
}

Wir rendern jetzt Daten aus der getPosts-Abfrage sowohl in einer Server-Komponente als auch in einer Client-Komponente. Das ist in Ordnung für das anfängliche Rendern der Seite, aber was passiert, wenn die Abfrage auf dem Client aus irgendeinem Grund neu validiert wird, wenn staleTime abgelaufen ist?

React Query weiß nicht, wie es die *Server-Komponente neu validieren* soll. Wenn es die Daten auf dem Client neu abruft und React veranlasst, die Liste der Beiträge neu zu rendern, werden die Anzahl der Beiträge: {posts.length} nicht mehr synchron sein.

Das ist in Ordnung, wenn Sie staleTime: Infinity setzen, so dass React Query niemals neu validiert, aber das ist wahrscheinlich nicht das, was Sie wollen, wenn Sie React Query überhaupt verwenden.

Die Verwendung von React Query mit Server-Komponenten macht am meisten Sinn, wenn

  • Sie eine App mit React Query haben und zu Server-Komponenten migrieren möchten, ohne die gesamte Datenabfrage neu zu schreiben
  • Sie ein vertrautes Programmiermodell wünschen, aber dennoch die Vorteile von Server-Komponenten nutzen möchten, wo es am sinnvollsten ist
  • Sie einen Anwendungsfall haben, den React Query abdeckt, aber Ihr gewähltes Framework nicht abdeckt

Es ist schwierig, allgemeine Ratschläge zu geben, wann es sinnvoll ist, React Query mit Server-Komponenten zu kombinieren und wann nicht. **Wenn Sie gerade erst mit einer neuen Server-Komponenten-App beginnen, empfehlen wir Ihnen, mit den Datenabruf-Tools Ihres Frameworks zu beginnen und React Query erst dann einzusetzen, wenn Sie es wirklich brauchen.** Das kann auch nie sein, und das ist in Ordnung, nutzen Sie das richtige Werkzeug für den Job!

Wenn Sie es verwenden, ist eine gute Faustregel, queryClient.fetchQuery zu vermeiden, es sei denn, Sie müssen Fehler abfangen. Wenn Sie es verwenden, rendern Sie dessen Ergebnis nicht auf dem Server und übergeben Sie das Ergebnis nicht an eine andere Komponente, auch nicht an eine Client-Komponente.

Aus der Perspektive von React Query sollten Sie Server-Komponenten als Ort zur Vorabrufung von Daten behandeln, nichts weiter.

Natürlich ist es in Ordnung, wenn Server-Komponenten einige Daten besitzen und Client-Komponenten andere, stellen Sie einfach sicher, dass diese beiden Realitäten nicht aus dem Gleichgewicht geraten.

Streaming mit Server-Komponenten

Der Next.js App Router streamt automatisch jeden Teil der Anwendung, der angezeigt werden kann, sobald wie möglich an den Browser, sodass fertige Inhalte sofort angezeigt werden können, ohne auf noch ausstehende Inhalte warten zu müssen. Dies geschieht entlang von <Suspense>-Grenzen. Beachten Sie, dass die Erstellung einer Datei loading.tsx im Hintergrund automatisch eine <Suspense>-Grenze erstellt.

Mit den oben beschriebenen Vorabrufmustern ist React Query perfekt mit dieser Art von Streaming kompatibel. Wenn die Daten für jede Suspense-Grenze aufgelöst werden, kann Next.js die fertigen Inhalte rendern und an den Browser streamen. Dies funktioniert auch dann, wenn Sie useQuery wie oben beschrieben verwenden, da das Suspendieren tatsächlich stattfindet, wenn Sie den Vorabruf awaiten.

Ab React Query v5.40.0 müssen Sie nicht mehr *alle* Vorabrufe awaiten, damit dies funktioniert, da *ausstehende* Abfragen auch dehydriert und an den Client gesendet werden können. Dies ermöglicht es Ihnen, Vorabrufe so früh wie möglich zu starten, ohne dass sie eine gesamte Suspense-Grenze blockieren, und streamt die *Daten* an den Client, sobald die Abfrage abgeschlossen ist. Dies kann zum Beispiel nützlich sein, wenn Sie Inhalte vorab abrufen möchten, die erst nach einer Benutzerinteraktion sichtbar sind, oder wenn Sie beispielsweise die erste Seite einer unendlichen Abfrage awaiten und rendern möchten, aber mit dem Vorabrufen von Seite 2 beginnen, ohne das Rendern zu blockieren.

Damit dies funktioniert, müssen wir den queryClient anweisen, auch ausstehende Abfragen zu dehydrieren. Wir können dies global tun oder indem wir diese Option direkt an dehydrate übergeben.

Wir müssen auch die getQueryClient()-Funktion aus unserer app/providers.tsx-Datei verschieben, da wir sie in unserer Server-Komponente und unserem Client-Provider verwenden möchten.

tsx
// app/get-query-client.ts
import {
  isServer,
  QueryClient,
  defaultShouldDehydrateQuery,
} from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
      },
      dehydrate: {
        // include pending queries in dehydration
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === 'pending',
        shouldRedactErrors: (error) => {
          // We should not catch Next.js server errors
          // as that's how Next.js detects dynamic pages
          // so we cannot redact them.
          // Next.js also automatically redacts errors for us
          // with better digests.
          return false
        },
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

export function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}
// app/get-query-client.ts
import {
  isServer,
  QueryClient,
  defaultShouldDehydrateQuery,
} from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
      },
      dehydrate: {
        // include pending queries in dehydration
        shouldDehydrateQuery: (query) =>
          defaultShouldDehydrateQuery(query) ||
          query.state.status === 'pending',
        shouldRedactErrors: (error) => {
          // We should not catch Next.js server errors
          // as that's how Next.js detects dynamic pages
          // so we cannot redact them.
          // Next.js also automatically redacts errors for us
          // with better digests.
          return false
        },
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

export function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

Hinweis: Dies funktioniert in Next.js und Server-Komponenten, weil React Promises über das Netzwerk serialisieren kann, wenn Sie sie an Client-Komponenten weitergeben.

Dann müssen wir nur noch einen HydrationBoundary bereitstellen, aber wir müssen Vorabrufe nicht mehr awaiten

tsx
// app/posts/page.tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { getQueryClient } from './get-query-client'
import Posts from './posts'

// the function doesn't need to be `async` because we don't `await` anything
export default function PostsPage() {
  const queryClient = getQueryClient()

  // look ma, no await
  queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}
// app/posts/page.tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { getQueryClient } from './get-query-client'
import Posts from './posts'

// the function doesn't need to be `async` because we don't `await` anything
export default function PostsPage() {
  const queryClient = getQueryClient()

  // look ma, no await
  queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}

Auf dem Client wird die Promise für uns in den QueryCache eingefügt. Das bedeutet, dass wir jetzt useSuspenseQuery innerhalb der Posts-Komponente aufrufen können, um diese Promise (die auf dem Server erstellt wurde) zu "verwenden".

tsx
// app/posts/posts.tsx
'use client'

export default function Posts() {
  const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })

  // ...
}
// app/posts/posts.tsx
'use client'

export default function Posts() {
  const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })

  // ...
}

Beachten Sie, dass Sie auch useQuery anstelle von useSuspenseQuery verwenden könnten, und die Promise würde trotzdem korrekt erkannt werden. Allerdings wird Next.js in diesem Fall nicht suspendieren, und die Komponente wird im *ausstehenden* Status gerendert, was auch das serverseitige Rendern des Inhalts ausschließt.

Wenn Sie nicht-JSON-Datentypen verwenden und die Abfrageergebnisse auf dem Server serialisieren, können Sie die Optionen dehydrate.serializeData und hydrate.deserializeData angeben, um die Daten auf jeder Seite der Grenze zu serialisieren und zu deserialisieren, um sicherzustellen, dass die Daten im Cache sowohl auf dem Server als auch auf dem Client im gleichen Format vorliegen.

tsx
// app/get-query-client.ts
import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
import { deserialize, serialize } from './transformer'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      // ...
      hydrate: {
        deserializeData: deserialize,
      },
      dehydrate: {
        serializeData: serialize,
      },
    },
  })
}

// ...
// app/get-query-client.ts
import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
import { deserialize, serialize } from './transformer'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      // ...
      hydrate: {
        deserializeData: deserialize,
      },
      dehydrate: {
        serializeData: serialize,
      },
    },
  })
}

// ...
tsx
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import { getQueryClient } from './get-query-client'
import { serialize } from './transformer'
import Posts from './posts'

export default function PostsPage() {
  const queryClient = getQueryClient()

  // look ma, no await
  queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}
// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import { getQueryClient } from './get-query-client'
import { serialize } from './transformer'
import Posts from './posts'

export default function PostsPage() {
  const queryClient = getQueryClient()

  // look ma, no await
  queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}
tsx
// app/posts/posts.tsx
'use client'

export default function Posts() {
  const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })

  // ...
}
// app/posts/posts.tsx
'use client'

export default function Posts() {
  const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })

  // ...
}

Nun kann Ihre getPosts-Funktion z. B. Temporal-Datums-/Zeitobjekte zurückgeben, und die Daten werden auf dem Client serialisiert und deserialisiert, vorausgesetzt, Ihr Transformer kann diese Datentypen serialisieren und deserialisieren.

Weitere Informationen finden Sie im Beispiel für Next.js App mit Prefetching.

Experimentelles Streaming ohne Vorabrufen in Next.js

Obwohl wir die oben beschriebene Vorabruflösung empfehlen, da sie Request-Wasserfälle sowohl beim anfänglichen Seitenaufruf *als auch* bei nachfolgenden Seitennavigationen abflacht, gibt es eine experimentelle Möglichkeit, den Vorabruf vollständig zu überspringen und trotzdem Streaming-SSR zu haben: @tanstack/react-query-next-experimental

Dieses Paket ermöglicht es Ihnen, Daten auf dem Server (in einer Client-Komponente) abzurufen, indem Sie einfach useSuspenseQuery in Ihrer Komponente aufrufen. Die Ergebnisse werden dann von Server zu Client gestreamt, wenn SuspenseBoundaries aufgelöst werden. Wenn Sie useSuspenseQuery aufrufen, ohne es in eine <Suspense>-Grenze zu packen, beginnt die HTML-Antwort erst, wenn der Abruf aufgelöst ist. Dies kann je nach Situation wünschenswert sein, aber bedenken Sie, dass dies Ihre TTFB beeinträchtigt.

Um dies zu erreichen, packen Sie Ihre App in die ReactQueryStreamedHydration-Komponente

tsx
// app/providers.tsx
'use client'

import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export function Providers(props: { children: React.ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>
        {props.children}
      </ReactQueryStreamedHydration>
    </QueryClientProvider>
  )
}
// app/providers.tsx
'use client'

import {
  isServer,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // With SSR, we usually want to set some default staleTime
        // above 0 to avoid refetching immediately on the client
        staleTime: 60 * 1000,
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
  if (isServer) {
    // Server: always make a new query client
    return makeQueryClient()
  } else {
    // Browser: make a new query client if we don't already have one
    // This is very important, so we don't re-make a new client if React
    // suspends during the initial render. This may not be needed if we
    // have a suspense boundary BELOW the creation of the query client
    if (!browserQueryClient) browserQueryClient = makeQueryClient()
    return browserQueryClient
  }
}

export function Providers(props: { children: React.ReactNode }) {
  // NOTE: Avoid useState when initializing the query client if you don't
  //       have a suspense boundary between this and the code that may
  //       suspend because React will throw away the client on the initial
  //       render if it suspends and there is no boundary
  const queryClient = getQueryClient()

  return (
    <QueryClientProvider client={queryClient}>
      <ReactQueryStreamedHydration>
        {props.children}
      </ReactQueryStreamedHydration>
    </QueryClientProvider>
  )
}

Weitere Informationen finden Sie im Beispiel für Next.js Suspense Streaming.

Der große Vorteil ist, dass Sie Abfragen nicht mehr manuell vorab abrufen müssen, damit SSR funktioniert, und es streamt die Ergebnisse trotzdem! Das sorgt für eine phänomenale DX und eine geringere Codekomplexität.

Der Nachteil lässt sich am einfachsten erklären, wenn wir uns das Beispiel für komplexe Request-Wasserfälle im Leitfaden für Performance & Request-Wasserfälle ansehen. Server-Komponenten mit Vorabruf eliminieren effektiv die Request-Wasserfälle sowohl für den anfänglichen Seitenaufruf *als auch* für nachfolgende Navigationen. Dieser Ansatz ohne Vorabruf glättet die Wasserfälle jedoch nur beim anfänglichen Seitenaufruf, führt aber bei Seitennavigationen zum gleichen tiefen Wasserfall wie im ursprünglichen Beispiel.

1. |> JS for <Feed>
2.   |> getFeed()
3.     |> JS for <GraphFeedItem>
4.       |> getGraphDataById()
1. |> JS for <Feed>
2.   |> getFeed()
3.     |> JS for <GraphFeedItem>
4.       |> getGraphDataById()

Dies ist sogar noch schlimmer als bei getServerSideProps/getStaticProps, da wir bei diesen zumindest Daten- und Codeabrufe parallelisieren konnten.

Wenn Sie DX/Iterations-/Shipping-Geschwindigkeit bei geringer Codekomplexität gegenüber Performance bevorzugen, keine tief verschachtelten Abfragen haben oder Ihre Request-Wasserfälle mit parallelem Abruf mit Tools wie useSuspenseQueries im Griff haben, kann dies ein guter Kompromiss sein.

Es ist möglicherweise möglich, die beiden Ansätze zu kombinieren, aber selbst wir haben das noch nicht ausprobiert. Wenn Sie dies tun, berichten Sie bitte über Ihre Ergebnisse oder aktualisieren Sie diese Dokumentation sogar mit einigen Tipps!

Abschließende Worte

Server-Komponenten und Streaming sind noch relativ neue Konzepte, und wir überlegen noch, wie React Query passt und welche Verbesserungen wir an der API vornehmen können. Wir freuen uns über Vorschläge, Feedback und Fehlerberichte!

Ebenso wäre es unmöglich, alle Feinheiten dieses neuen Paradigmas in einem einzigen Leitfaden beim ersten Versuch zu vermitteln. Wenn Sie hier einige Informationen vermissen oder Vorschläge zur Verbesserung dieses Inhalts haben, nehmen Sie ebenfalls Kontakt mit uns auf, oder noch besser, klicken Sie auf die Schaltfläche "Edit on GitHub" unten und helfen Sie uns.

Weiterführende Lektüre

Um zu verstehen, ob Ihre Anwendung von React Query profitieren kann, wenn Sie auch Server-Komponenten verwenden, schauen Sie sich You Might Not Need React Query aus den Community-Ressourcen an.