Wenn Sie wissen oder vermuten, dass ein bestimmter Datensatz benötigt wird, können Sie Prefetching verwenden, um den Cache im Voraus mit diesen Daten zu füllen, was zu einer schnelleren Erfahrung führt.
Es gibt verschiedene Prefetching-Muster
In dieser Anleitung werden wir uns die ersten drei ansehen, während die vierte ausführlich im Leitfaden Server Rendering & Hydration und im Leitfaden Advanced Server Rendering behandelt wird.
Ein spezifischer Anwendungsfall für Prefetching ist die Vermeidung von Request Waterfalls. Eine eingehende Hintergrundinformation und Erklärung dazu finden Sie im Leitfaden Performance & Request Waterfalls.
Bevor wir uns mit den verschiedenen spezifischen Prefetch-Mustern befassen, werfen wir einen Blick auf die Funktionen prefetchQuery und prefetchInfiniteQuery. Zuerst ein paar Grundlagen
So verwenden Sie prefetchQuery
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
Infinite Queries können wie reguläre Queries vorgefetcht werden. Standardmäßig wird nur die erste Seite der Query vorgefetcht und unter dem angegebenen QueryKey gespeichert. Wenn Sie mehr als eine Seite vorfetchen möchten, können Sie die Option pages verwenden, in diesem Fall müssen Sie auch eine getNextPageParam-Funktion angeben.
const prefetchProjects = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
})
}
const prefetchProjects = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
pages: 3, // prefetch the first 3 pages
})
}
Als Nächstes sehen wir uns an, wie Sie diese und andere Möglichkeiten nutzen können, um in verschiedenen Situationen zu prefetchten.
Eine einfache Form des Prefetchings ist, es auszuführen, wenn der Benutzer mit etwas interagiert. In diesem Beispiel verwenden wir queryClient.prefetchQuery, um einen Prefetch bei onMouseEnter oder onFocus zu starten.
function ShowDetailsButton() {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['details'],
queryFn: getDetailsData,
// Prefetch only fires when data is older than the staleTime,
// so in a case like this you definitely want to set one
staleTime: 60000,
})
}
return (
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
Show Details
</button>
)
}
function ShowDetailsButton() {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['details'],
queryFn: getDetailsData,
// Prefetch only fires when data is older than the staleTime,
// so in a case like this you definitely want to set one
staleTime: 60000,
})
}
return (
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}>
Show Details
</button>
)
}
Prefetching während des Komponentenlebenszyklus ist nützlich, wenn wir wissen, dass ein Kind oder Nachfahre einen bestimmten Datensatz benötigt, aber wir können diesen nicht rendern, bis eine andere Abfrage abgeschlossen ist. Nehmen wir ein Beispiel aus dem Request Waterfall Guide, um dies zu erklären
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
Dies führt zu einem Request Waterfall, der wie folgt aussieht:
1. |> getArticleById()
2. |> getArticleCommentsById()
1. |> getArticleById()
2. |> getArticleCommentsById()
Wie in diesem Leitfaden erwähnt, besteht eine Möglichkeit, diesen Waterfall abzuflachen und die Leistung zu verbessern, darin, die Abfrage getArticleCommentsById in die übergeordnete Komponente zu verschieben und das Ergebnis als Prop weiterzugeben. Aber was, wenn dies nicht machbar oder wünschenswert ist, z. B. wenn die Komponenten nicht miteinander verbunden sind und mehrere Ebenen dazwischen liegen?
In diesem Fall können wir stattdessen die Abfrage in der übergeordneten Komponente prefetchten. Der einfachste Weg, dies zu tun, ist die Verwendung einer Abfrage, aber das Ergebnis zu ignorieren.
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
// Prefetch
useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
// Optional optimization to avoid rerenders when this query changes:
notifyOnChangeProps: [],
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
function Article({ id }) {
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: getArticleById,
})
// Prefetch
useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
// Optional optimization to avoid rerenders when this query changes:
notifyOnChangeProps: [],
})
if (isPending) {
return 'Loading article...'
}
return (
<>
<ArticleHeader articleData={articleData} />
<ArticleBody articleData={articleData} />
<Comments id={id} />
</>
)
}
function Comments({ id }) {
const { data, isPending } = useQuery(() => {
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
...
}
Dies startet das Abrufen von 'article-comments' sofort und glättet den Waterfall.
1. |> getArticleById()
1. |> getArticleCommentsById()
1. |> getArticleById()
1. |> getArticleCommentsById()
Wenn Sie zusammen mit Suspense prefetchten möchten, müssen Sie die Dinge etwas anders handhaben. Sie können useSuspenseQueries nicht zum Prefetching verwenden, da das Prefetching das Rendern der Komponente blockieren würde. Sie können auch useQuery nicht für das Prefetching verwenden, da dies das Prefetching erst nach Abschluss der suspenseful Query starten würde. Für dieses Szenario können Sie die in der Bibliothek verfügbaren Hooks usePrefetchQuery oder usePrefetchInfiniteQuery verwenden.
Sie können nun useSuspenseQuery in der Komponente verwenden, die die Daten tatsächlich benötigt. Möglicherweise möchten Sie diese spätere Komponente in ihre eigene <Suspense>-Grenze einwickeln, damit die "sekundäre" Abfrage, die wir prefetchten, das Rendern der "primären" Daten nicht blockiert.
function ArticleLayout({ id }) {
usePrefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
return (
<Suspense fallback="Loading article">
<Article id={id} />
</Suspense>
)
}
function Article({ id }) {
const { data: articleData, isPending } = useSuspenseQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
...
}
function ArticleLayout({ id }) {
usePrefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
return (
<Suspense fallback="Loading article">
<Article id={id} />
</Suspense>
)
}
function Article({ id }) {
const { data: articleData, isPending } = useSuspenseQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
...
}
Eine andere Möglichkeit ist, innerhalb der Abfragefunktion zu prefetchten. Dies ist sinnvoll, wenn Sie wissen, dass jedes Mal, wenn ein Artikel abgerufen wird, wahrscheinlich auch Kommentare benötigt werden. Dazu verwenden wir queryClient.prefetchQuery.
const queryClient = useQueryClient()
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: (...args) => {
queryClient.prefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
return getArticleById(...args)
},
})
const queryClient = useQueryClient()
const { data: articleData, isPending } = useQuery(() => {
queryKey: ['article', id],
queryFn: (...args) => {
queryClient.prefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
return getArticleById(...args)
},
})
Prefetching in einem Effekt funktioniert ebenfalls, aber beachten Sie, dass, wenn Sie useSuspenseQuery in derselben Komponente verwenden, dieser Effekt erst ausgeführt wird, *nachdem* die Abfrage abgeschlossen ist, was möglicherweise nicht das ist, was Sie möchten.
const queryClient = useQueryClient()
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
}, [queryClient, id])
const queryClient = useQueryClient()
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
}, [queryClient, id])
Zusammenfassend lässt sich sagen, dass es verschiedene Möglichkeiten gibt, eine Abfrage während des Komponentenlebenszyklus zu prefetchten. Wählen Sie diejenige, die am besten zu Ihrer Situation passt.
Als Nächstes betrachten wir einen etwas fortgeschritteneren Fall.
Manchmal möchten wir bedingt prefetchten, basierend auf dem Ergebnis eines anderen Fetches. Betrachten Sie dieses Beispiel aus dem Leitfaden Performance & Request Waterfalls.
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
// This lazy loads the GraphFeedItem component, meaning
// it wont start loading until something renders it
const GraphFeedItem = Solid.lazy(() => import('./GraphFeedItem'))
function Feed() {
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: getFeed,
})
if (isPending) {
return 'Loading feed...'
}
return (
<>
{data.map((feedItem) => {
if (feedItem.type === 'GRAPH') {
return <GraphFeedItem key={feedItem.id} feedItem={feedItem} />
}
return <StandardFeedItem key={feedItem.id} feedItem={feedItem} />
})}
</>
)
}
// GraphFeedItem.tsx
function GraphFeedItem({ feedItem }) {
const { data, isPending } = useQuery(() => {
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
...
}
Wie in diesem Leitfaden erwähnt, führt dieses Beispiel zu dem folgenden doppelten Request Waterfall:
1. |> getFeed()
2. |> JS for <GraphFeedItem>
3. |> getGraphDataById()
1. |> getFeed()
2. |> JS for <GraphFeedItem>
3. |> getGraphDataById()
Wenn wir unsere API nicht umstrukturieren können, sodass getFeed() bei Bedarf auch die Daten von getGraphDataById() zurückgibt, gibt es keine Möglichkeit, den Waterfall getFeed->getGraphDataById zu beseitigen. Aber durch die Nutzung von bedingtem Prefetching können wir zumindest den Code und die Daten parallel laden. Ähnlich wie oben beschrieben gibt es mehrere Möglichkeiten, dies zu tun, aber für dieses Beispiel werden wir es in der Abfragefunktion tun.
function Feed() {
const queryClient = useQueryClient()
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: async (...args) => {
const feed = await getFeed(...args)
for (const feedItem of feed) {
if (feedItem.type === 'GRAPH') {
queryClient.prefetchQuery({
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
}
}
return feed
}
})
...
}
function Feed() {
const queryClient = useQueryClient()
const { data, isPending } = useQuery(() => {
queryKey: ['feed'],
queryFn: async (...args) => {
const feed = await getFeed(...args)
for (const feedItem of feed) {
if (feedItem.type === 'GRAPH') {
queryClient.prefetchQuery({
queryKey: ['graph', feedItem.id],
queryFn: getGraphDataById,
})
}
}
return feed
}
})
...
}
Dies würde den Code und die Daten parallel laden.
1. |> getFeed()
2. |> JS for <GraphFeedItem>
2. |> getGraphDataById()
1. |> getFeed()
2. |> JS for <GraphFeedItem>
2. |> getGraphDataById()
Es gibt jedoch einen Kompromiss: Der Code für getGraphDataById ist jetzt im übergeordneten Bundle enthalten und nicht im JS für <GraphFeedItem>. Sie müssen also von Fall zu Fall entscheiden, welcher der beste Kompromiss für die Leistung ist. Wenn GraphFeedItem wahrscheinlich sind, lohnt es sich wahrscheinlich, den Code in der übergeordneten Komponente einzuschließen. Wenn sie extrem selten sind, wahrscheinlich nicht.
Da das Daten-Fetching im Komponentenbaum selbst leicht zu Request Waterfalls führen kann und die verschiedenen Behebungen dafür mit zunehmender Anwendung umständlich werden können, ist die Integration von Prefetching auf Router-Ebene eine attraktive Möglichkeit.
Bei diesem Ansatz deklarieren Sie explizit für jede *Route*, welche Daten für diesen Komponentenbaum im Voraus benötigt werden. Da Server-Rendering traditionell alle Daten vor Beginn des Renderns geladen haben musste, war dies lange Zeit der dominierende Ansatz für SSR-fähige Apps. Dies ist immer noch ein gängiger Ansatz, und Sie können mehr darüber im Leitfaden Server Rendering & Hydration erfahren.
Fürs Erste konzentrieren wir uns auf den Client-seitigen Fall und sehen uns ein Beispiel an, wie Sie dies mit Tanstack Router umsetzen können. Diese Beispiele lassen viel Einrichtung und Boilerplate weg, um prägnant zu bleiben. Ein vollständiges React Query-Beispiel finden Sie in den Tanstack Router-Dokumenten.
Bei der Integration auf Router-Ebene können Sie wählen, ob das Rendern der Route blockiert werden soll, bis alle Daten vorhanden sind, oder ob Sie einen Prefetch starten, aber nicht auf das Ergebnis warten. Auf diese Weise können Sie das Rendern der Route so schnell wie möglich starten. Sie können diese beiden Ansätze auch mischen und auf einige kritische Daten warten, aber mit dem Rendern beginnen, bevor alle sekundären Daten geladen sind. In diesem Beispiel konfigurieren wir eine Route /article, die nicht gerendert wird, bis die Artikeldaten geladen sind, und starten gleichzeitig das Prefetching von Kommentaren so schnell wie möglich, ohne jedoch das Rendern der Route zu blockieren, wenn die Kommentare noch nicht geladen sind.
const queryClient = new QueryClient()
const routerContext = new RouterContext()
const rootRoute = routerContext.createRootRoute({
component: () => { ... }
})
const articleRoute = new Route({
getParentRoute: () => rootRoute,
path: 'article',
beforeLoad: () => {
return {
articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle },
commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments },
}
},
loader: async ({
context: { queryClient },
routeContext: { articleQueryOptions, commentsQueryOptions },
}) => {
// Fetch comments asap, but don't block
queryClient.prefetchQuery(commentsQueryOptions)
// Don't render the route at all until article has been fetched
await queryClient.prefetchQuery(articleQueryOptions)
},
component: ({ useRouteContext }) => {
const { articleQueryOptions, commentsQueryOptions } = useRouteContext()
const articleQuery = useQuery(() => articleQueryOptions)
const commentsQuery = useQuery(() => commentsQueryOptions)
return (
...
)
},
errorComponent: () => 'Oh crap!',
})
const queryClient = new QueryClient()
const routerContext = new RouterContext()
const rootRoute = routerContext.createRootRoute({
component: () => { ... }
})
const articleRoute = new Route({
getParentRoute: () => rootRoute,
path: 'article',
beforeLoad: () => {
return {
articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle },
commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments },
}
},
loader: async ({
context: { queryClient },
routeContext: { articleQueryOptions, commentsQueryOptions },
}) => {
// Fetch comments asap, but don't block
queryClient.prefetchQuery(commentsQueryOptions)
// Don't render the route at all until article has been fetched
await queryClient.prefetchQuery(articleQueryOptions)
},
component: ({ useRouteContext }) => {
const { articleQueryOptions, commentsQueryOptions } = useRouteContext()
const articleQuery = useQuery(() => articleQueryOptions)
const commentsQuery = useQuery(() => commentsQueryOptions)
return (
...
)
},
errorComponent: () => 'Oh crap!',
})
Die Integration mit anderen Routern ist ebenfalls möglich. Sehen Sie sich das Beispiel react-router für eine weitere Demonstration an.
Wenn Sie die Daten für Ihre Abfrage bereits synchron verfügbar haben, müssen Sie sie nicht prefetchten. Sie können einfach die Methode setQueryData des Query Clients verwenden, um das zwischengespeicherte Ergebnis einer Abfrage direkt anhand des Schlüssels hinzuzufügen oder zu aktualisieren.
queryClient.setQueryData(['todos'], todos)
queryClient.setQueryData(['todos'], todos)
Für eine detaillierte Anleitung, wie Sie Daten in Ihren Query Cache bekommen, bevor Sie fetchen, sehen Sie sich #17: Seeding the Query Cache aus den Community Resources an.
Die Integration mit Server-seitigen Routern und Frameworks ist sehr ähnlich zu dem, was wir gerade gesehen haben, mit der zusätzlichen Anforderung, dass die Daten vom Server an den Client übergeben werden müssen, um dort in den Cache hydriert zu werden. Um zu erfahren, wie das geht, fahren Sie mit dem Leitfaden Server Rendering & Hydration fort.