initialDataIn diesem Leitfaden lernen Sie, wie Sie React Query mit Server-Rendering verwenden.
Siehe den Leitfaden zu Prefetching & Router-Integration für einige Hintergrundinformationen. Möglicherweise möchten Sie auch den Leitfaden Performance & Request Waterfalls davor lesen.
Für fortgeschrittene Server-Rendering-Muster wie Streaming, Server Components und den neuen Next.js App Router siehe den Leitfaden für erweitertes SSR.
Wenn Sie nur etwas Code sehen möchten, können Sie zum Beispiel für den vollständigen Next.js Pages Router oder zum vollständigen Remix-Beispiel unten springen.
Was ist Server-Rendering überhaupt? Der Rest dieses Leitfadens geht davon aus, dass Sie mit dem Konzept vertraut sind, aber lassen Sie uns kurz darauf eingehen, wie es sich auf React Query auswirkt. Server-Rendering ist die Erstellung des anfänglichen HTML auf dem Server, damit der Benutzer sofort nach dem Laden der Seite Inhalt sehen kann. Dies kann bei Bedarf erfolgen, wenn eine Seite angefordert wird (SSR). Es kann auch im Voraus erfolgen, entweder weil eine vorherige Anfrage zwischengespeichert wurde oder zur Build-Zeit (SSG).
Wenn Sie den Leitfaden zu Request Waterfalls gelesen haben, erinnern Sie sich vielleicht daran
1. |-> Markup (without content)
2. |-> JS
3. |-> Query
1. |-> Markup (without content)
2. |-> JS
3. |-> Query
Bei einer clientseitig gerenderten Anwendung sind dies die mindestens 3 Server-Roundtrips, die Sie benötigen, bevor der Benutzer überhaupt Inhalte auf dem Bildschirm sieht. Eine Möglichkeit, Server-Rendering zu betrachten, ist, dass es das obige in dieses umwandelt
1. |-> Markup (with content AND initial data)
2. |-> JS
1. |-> Markup (with content AND initial data)
2. |-> JS
Sobald **1.** abgeschlossen ist, kann der Benutzer den Inhalt sehen, und wenn **2.** beendet ist, ist die Seite interaktiv und klickbar. Da das Markup auch die benötigten Anfangsdaten enthält, muss Schritt **3.** auf dem Client überhaupt nicht ausgeführt werden, zumindest nicht, bis Sie die Daten aus irgendeinem Grund erneut validieren möchten.
Dies ist alles aus der Perspektive des Clients. Auf dem Server müssen wir diese Daten vorab abrufen, bevor wir das Markup generieren/rendern, wir müssen diese Daten in ein serialisierbares Format umwandeln (dehydrieren), das wir in das Markup einbetten können, und auf dem Client müssen wir diese Daten in einen React Query Cache einspeisen (hydrieren), damit wir einen neuen Fetch auf dem Client vermeiden können.
Lesen Sie weiter, um zu erfahren, wie Sie diese drei Schritte mit React Query implementieren.
Dieser Leitfaden verwendet die reguläre useQuery API. Obwohl wir sie nicht unbedingt empfehlen, ist es möglich, sie durch useSuspenseQuery zu ersetzen, **vorausgesetzt, Sie rufen alle Ihre Queries vorab ab**. Der Vorteil ist, dass Sie <Suspense> für Ladezustände auf dem Client verwenden können.
Wenn Sie vergessen, eine Query vorab abzurufen, während Sie useSuspenseQuery verwenden, hängen die Folgen vom verwendeten Framework ab. In einigen Fällen wird die Abfrage aussetzen und auf dem Server abgerufen, aber nie an den Client übermittelt, wo sie erneut abgerufen wird. In diesen Fällen erhalten Sie einen Markup-Hydrations-Mismatch, da der Server und der Client versucht haben, unterschiedliche Dinge zu rendern.
Die ersten Schritte bei der Verwendung von React Query bestehen immer darin, einen queryClient zu erstellen und die Anwendung in einen <QueryClientProvider> zu verpacken. Beim Server-Rendering ist es wichtig, die queryClient-Instanz **innerhalb Ihrer App** im React-State (ein Instanz-Ref funktioniert ebenfalls) zu erstellen. **Dies stellt sicher, dass Daten nicht zwischen verschiedenen Benutzern und Anfragen geteilt werden**, während gleichzeitig nur eine queryClient pro Komponentenlebenszyklus erstellt wird.
Next.js Pages Router
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// NEVER DO THIS:
// const queryClient = new QueryClient()
//
// Creating the queryClient at the file root level makes the cache shared
// between all requests and means _all_ data gets passed to _all_ users.
// Besides being bad for performance, this also leaks any sensitive data.
export default function MyApp({ Component, pageProps }) {
// Instead do this, which ensures each request has its own cache:
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// NEVER DO THIS:
// const queryClient = new QueryClient()
//
// Creating the queryClient at the file root level makes the cache shared
// between all requests and means _all_ data gets passed to _all_ users.
// Besides being bad for performance, this also leaks any sensitive data.
export default function MyApp({ Component, pageProps }) {
// Instead do this, which ensures each request has its own cache:
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
Remix
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
Der schnellste Weg, um loszulegen, ist, React Query überhaupt nicht für das Vorabrufen einzubeziehen und die dehydrate/hydrate APIs nicht zu verwenden. Stattdessen übergeben Sie die Rohdaten als Option initialData an useQuery. Schauen wir uns ein Beispiel mit dem Next.js Pages Router und getServerSideProps an.
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
Dies funktioniert auch mit getStaticProps oder sogar dem älteren getInitialProps, und das gleiche Muster kann in jedem anderen Framework mit entsprechenden Funktionen angewendet werden. So sieht das gleiche Beispiel mit Remix aus
export async function loader() {
const posts = await getPosts()
return json({ posts })
}
function Posts() {
const { posts } = useLoaderData<typeof loader>()
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: posts,
})
// ...
}
export async function loader() {
const posts = await getPosts()
return json({ posts })
}
function Posts() {
const { posts } = useLoaderData<typeof loader>()
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: posts,
})
// ...
}
Die Einrichtung ist minimal und dies kann eine schnelle Lösung für einige Fälle sein, aber es gibt ein paar **Kompromisse zu beachten**, verglichen mit dem vollständigen Ansatz
Die Einrichtung der vollständigen Hydrationslösung ist unkompliziert und hat diese Nachteile nicht. Dies wird der Schwerpunkt für den Rest der Dokumentation sein.
Mit nur wenig mehr Einrichtung können Sie einen queryClient verwenden, um Queries während einer Vorabrufphase abzurufen, eine serialisierte Version dieses queryClient an den Rendering-Teil der App zu übergeben und ihn dort wiederzuverwenden. Dies vermeidet die oben genannten Nachteile. Sie können gerne zu den vollständigen Beispielen für den Next.js Pages Router und Remix springen, aber auf allgemeiner Ebene sind dies die zusätzlichen Schritte
Ein interessantes Detail ist, dass tatsächlich *drei* queryClients beteiligt sind. Die Framework-Loader sind eine Form der "Vorabruf"-Phase, die vor dem Rendering stattfindet, und diese Phase hat ihren eigenen queryClient, der das Vorabrufen durchführt. Das dehydrierte Ergebnis dieser Phase wird **sowohl** an den Server-Rendering-Prozess **als auch** an den Client-Rendering-Prozess übergeben, die jeweils ihren eigenen queryClient haben. Dies stellt sicher, dass beide mit denselben Daten beginnen, damit sie dasselbe Markup zurückgeben können.
Server Components sind eine weitere Form der "Vorabruf"-Phase, die auch Teile eines React-Komponentenbaums "vorabrufen" (vorrendern) kann. Lesen Sie mehr im Leitfaden für erweitertes SSR.
Für die App Router-Dokumentation siehe den Leitfaden für erweitertes SSR.
Ersteinrichtung
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
)
}
In jeder Route
// 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
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
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>
)
}
Ersteinrichtung
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
// app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function MyApp() {
const [queryClient] = React.useState(
() =>
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,
},
},
}),
)
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}
In jeder Route, beachten Sie, dass es in Ordnung ist, dies auch in verschachtelten Routen zu tun
// app/routes/posts.tsx
import { json } from '@remix-run/node'
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
export async function loader() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return json({ 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
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() {
const { dehydratedState } = useLoaderData<typeof loader>()
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
// app/routes/posts.tsx
import { json } from '@remix-run/node'
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
export async function loader() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return json({ 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
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() {
const { dehydratedState } = useLoaderData<typeof loader>()
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
Diese Komponente in jeder Route zu haben, mag wie viel Boilerplate erscheinen
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
Auch wenn an diesem Ansatz nichts falsch ist, wenn Sie diese Boilerplate entfernen möchten, hier ist, wie Sie Ihre Einrichtung in Next.js ändern können
// _app.tsx
import {
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
)
}
// pages/posts.tsx
// Remove PostsRoute with the HydrationBoundary and instead export Posts directly:
export default function Posts() { ... }
// _app.tsx
import {
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
</QueryClientProvider>
)
}
// pages/posts.tsx
// Remove PostsRoute with the HydrationBoundary and instead export Posts directly:
export default function Posts() { ... }
Mit Remix ist dies etwas aufwendiger. Wir empfehlen die Überprüfung des Pakets use-dehydrated-state.
Im Prefetching-Leitfaden haben wir gelernt, wie man abhängige Queries vorab abruft, aber wie machen wir das in Framework-Loadern? Betrachten Sie den folgenden Code aus dem Leitfaden Dependent Queries
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
Wie würden wir das vorab abrufen, damit es servergerendert werden kann? Hier ist ein Beispiel
// For Remix, rename this to loader instead
export async function getServerSideProps() {
const queryClient = new QueryClient()
const user = await queryClient.fetchQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
if (user?.userId) {
await queryClient.prefetchQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
})
}
// For Remix:
// return json({ dehydratedState: dehydrate(queryClient) })
return { props: { dehydratedState: dehydrate(queryClient) } }
}
// For Remix, rename this to loader instead
export async function getServerSideProps() {
const queryClient = new QueryClient()
const user = await queryClient.fetchQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
if (user?.userId) {
await queryClient.prefetchQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
})
}
// For Remix:
// return json({ dehydratedState: dehydrate(queryClient) })
return { props: { dehydratedState: dehydrate(queryClient) } }
}
Dies kann natürlich komplexer werden, aber da diese Loader-Funktionen nur JavaScript sind, können Sie die volle Kraft der Sprache nutzen, um Ihre Logik zu entwickeln. Stellen Sie sicher, dass Sie alle Queries vorab abrufen, die servergerendert werden sollen.
React Query setzt standardmäßig auf eine Strategie der graduellen Verschlechterung. Das bedeutet
Dies führt dazu, dass fehlgeschlagene Queries auf dem Client erneut versucht werden und die servergerenderte Ausgabe Ladezustände anstelle des vollständigen Inhalts enthält.
Obwohl dies eine gute Standardeinstellung ist, ist es manchmal nicht das, was Sie wollen. Wenn kritische Inhalte fehlen, möchten Sie möglicherweise je nach Situation mit einem 404- oder 500-Statuscode antworten. Für diese Fälle verwenden Sie stattdessen queryClient.fetchQuery(...), das bei Fehlern Fehler auslöst und es Ihnen ermöglicht, die Dinge entsprechend zu handhaben.
let result
try {
result = await queryClient.fetchQuery(...)
} catch (error) {
// Handle the error, refer to your framework documentation
}
// You might also want to check and handle any invalid `result` here
let result
try {
result = await queryClient.fetchQuery(...)
} catch (error) {
// Handle the error, refer to your framework documentation
}
// You might also want to check and handle any invalid `result` here
Wenn Sie aus irgendeinem Grund fehlgeschlagene Queries in den dehydrierten Zustand aufnehmen möchten, um Wiederholungsversuche zu vermeiden, können Sie die Option shouldDehydrateQuery verwenden, um die Standardfunktion zu überschreiben und Ihre eigene Logik zu implementieren
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// This will include all queries, including failed ones,
// but you can also implement your own logic by inspecting `query`
return true
},
})
dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// This will include all queries, including failed ones,
// but you can also implement your own logic by inspecting `query`
return true
},
})
Wenn Sie in Next.js return { props: { dehydratedState: dehydrate(queryClient) } } oder in Remix return json({ dehydratedState: dehydrate(queryClient) }) zurückgeben, wird die dehydratedState-Darstellung des queryClient vom Framework serialisiert, damit sie in das Markup eingebettet und zum Client transportiert werden kann.
Standardmäßig unterstützen diese Frameworks nur das Zurückgeben von Dingen, die sicher serialisierbar/parsierbar sind, und unterstützen daher keine undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, -0, reguläre Ausdrücke usw. Das bedeutet auch, dass Sie keine dieser Werte aus Ihren Queries zurückgeben können. Wenn Sie diese Werte zurückgeben möchten, prüfen Sie superjson oder ähnliche Pakete.
Wenn Sie eine benutzerdefinierte SSR-Einrichtung verwenden, müssen Sie diesen Schritt selbst durchführen. Ihr erster Instinkt könnte sein, JSON.stringify(dehydratedState) zu verwenden, aber da dies standardmäßig keine Dinge wie <script>alert('Oh no..')</script> maskiert, kann dies leicht zu **XSS-Schwachstellen** in Ihrer Anwendung führen. superjson maskiert ebenfalls keine Werte und ist für sich allein in einer benutzerdefinierten SSR-Einrichtung unsicher (es sei denn, Sie fügen einen zusätzlichen Schritt zum Maskieren der Ausgabe hinzu). Stattdessen empfehlen wir die Verwendung einer Bibliothek wie Serialize JavaScript oder devalue, die beide sofort sicher gegen XSS-Injektionen sind.
Im Leitfaden Performance & Request Waterfalls haben wir erwähnt, dass wir uns ansehen werden, wie Server-Rendering einen der komplexeren verschachtelten Waterfalls verändert. Schauen Sie sich den spezifischen Codebeispiel an, aber zur Erinnerung: Wir haben eine Code-Split <GraphFeedItem>-Komponente innerhalb einer <Feed>-Komponente. Diese rendert nur, wenn der Feed ein Graph-Element enthält, und beide Komponenten rufen ihre eigenen Daten ab. Mit Client-Rendering führt dies zu folgendem Request Waterfall
1. |> Markup (without content)
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
1. |> Markup (without content)
2. |> JS for <Feed>
3. |> getFeed()
4. |> JS for <GraphFeedItem>
5. |> getGraphDataById()
Das Schöne am Server-Rendering ist, dass wir das obige in
1. |> Markup (with content AND initial data)
2. |> JS for <Feed>
2. |> JS for <GraphFeedItem>
1. |> Markup (with content AND initial data)
2. |> JS for <Feed>
2. |> JS for <GraphFeedItem>
Beachten Sie, dass die Queries nicht mehr auf dem Client abgerufen werden, stattdessen waren ihre Daten im Markup enthalten. Der Grund, warum wir jetzt den JS parallel laden können, ist, dass, da <GraphFeedItem> auf dem Server gerendert wurde, wir wissen, dass wir diesen JS auch auf dem Client benötigen werden und ein Skript-Tag für diesen Chunk in das Markup einfügen können. Auf dem Server hätten wir immer noch diesen Request Waterfall
1. |> getFeed()
2. |> getGraphDataById()
1. |> getFeed()
2. |> getGraphDataById()
Wir können einfach nicht wissen, nachdem wir den Feed abgerufen haben, ob wir auch Graph-Daten abrufen müssen. Es sind abhängige Queries. Da dies auf dem Server geschieht, wo die Latenz im Allgemeinen sowohl niedriger als auch stabiler ist, ist dies oft kein großes Problem.
Fantastisch, wir haben unsere Waterfalls größtenteils abgeflacht! Es gibt jedoch einen Haken. Nennen wir diese Seite die /feed-Seite, und nehmen wir an, wir haben auch eine andere Seite wie /posts. Wenn wir www.example.com/feed direkt in die Adressleiste eingeben und Enter drücken, erhalten wir all diese großartigen Server-Rendering-Vorteile, ABER, wenn wir stattdessen www.example.com/posts eingeben und dann **auf einen Link** zu /feed klicken, sind wir wieder hier
1. |> JS for <Feed>
2. |> getFeed()
3. |> JS for <GraphFeedItem>
4. |> getGraphDataById()
1. |> JS for <Feed>
2. |> getFeed()
3. |> JS for <GraphFeedItem>
4. |> getGraphDataById()
Das liegt daran, dass bei SPAs das Server-Rendering nur für den anfänglichen Seitenaufruf funktioniert, nicht für nachfolgende Navigationen.
Moderne Frameworks versuchen oft, dies zu lösen, indem sie den anfänglichen Code und die Daten parallel abrufen. Wenn Sie also Next.js oder Remix mit den hier skizzierten Vorabrufmustern verwenden würden, einschließlich des Vorabrufs abhängiger Queries, würde es stattdessen so aussehen
1. |> JS for <Feed>
1. |> getFeed() + getGraphDataById()
2. |> JS for <GraphFeedItem>
1. |> JS for <Feed>
1. |> getFeed() + getGraphDataById()
2. |> JS for <GraphFeedItem>
Das ist viel besser, aber wenn wir das noch weiter verbessern wollen, können wir es mit Server Components auf einen einzigen Roundtrip abflachen. Erfahren Sie mehr im Leitfaden für erweitertes SSR.
Eine Query gilt je nachdem, wann sie dataUpdatedAt wurde, als veraltet. Ein Vorbehalt ist hier, dass der Server die richtige Uhrzeit haben muss, damit dies ordnungsgemäß funktioniert. Es wird jedoch die UTC-Zeit verwendet, sodass Zeitzonen hier nicht berücksichtigt werden.
Da staleTime standardmäßig auf 0 gesetzt ist, werden Queries standardmäßig im Hintergrund beim Laden der Seite erneut abgerufen. Sie möchten möglicherweise eine höhere staleTime verwenden, um diesen doppelten Abruf zu vermeiden, insbesondere wenn Sie Ihr Markup nicht cachen.
Diese erneute Abfrage veralteter Queries passt perfekt zum Caching von Markup in einem CDN! Sie können die Cache-Zeit der Seite selbst recht hoch einstellen, um das erneute Rendern von Seiten auf dem Server zu vermeiden, aber konfigurieren Sie die staleTime der Queries niedriger, um sicherzustellen, dass Daten im Hintergrund erneut abgerufen werden, sobald ein Benutzer die Seite besucht. Vielleicht möchten Sie die Seiten eine Woche lang cachen, aber die Daten beim Laden der Seite automatisch erneut abrufen, wenn sie älter als ein Tag sind?
Falls Sie den QueryClient für jede Anfrage erstellen, erstellt React Query den isolierten Cache für diesen Client, der für die gcTime-Periode im Speicher erhalten bleibt. Dies kann bei einer hohen Anzahl von Anfragen während dieses Zeitraums zu einem hohen Speicherverbrauch auf dem Server führen.
Auf dem Server ist gcTime standardmäßig auf Infinity gesetzt, was die manuelle Garbage Collection deaktiviert und den Speicher automatisch löscht, sobald eine Anfrage abgeschlossen ist. Wenn Sie explizit eine nicht-unendliche gcTime einstellen, sind Sie dafür verantwortlich, den Cache frühzeitig zu löschen.
Vermeiden Sie es, gcTime auf 0 zu setzen, da dies zu einem Hydrationsfehler führen kann. Dies geschieht, weil die Hydration Boundary notwendige Daten in den Cache zum Rendern legt, aber wenn der Garbage Collector die Daten löscht, bevor das Rendern abgeschlossen ist, können Probleme auftreten. Wenn Sie eine kürzere gcTime benötigen, empfehlen wir, sie auf 2 * 1000 zu setzen, um ausreichend Zeit für die Anwendung zu lassen, auf die Daten zuzugreifen.
Um den Cache zu löschen, nachdem er nicht mehr benötigt wird, und den Speicherverbrauch zu senken, können Sie einen Aufruf zu queryClient.clear() hinzufügen, nachdem die Anfrage bearbeitet und der dehydrierte Zustand an den Client gesendet wurde.
Alternativ können Sie eine kleinere gcTime einstellen.
Es gibt einen Haken, wenn Sie Next.js' Rewrites-Funktion zusammen mit Automatic Static Optimization oder getStaticProps verwenden: Dies führt zu einer zweiten Hydration durch React Query. Das liegt daran, dass Next.js die Rewrites auf dem Client parsen und nach der Hydration alle Parameter sammeln muss, damit sie in router.query bereitgestellt werden können.
Das Ergebnis ist eine fehlende referenzielle Gleichheit für alle Hydrationsdaten, was beispielsweise dazu führt, wo immer Ihre Daten als Props von Komponenten oder im Abhängigkeitsarray von useEffects/useMemos verwendet werden.