Wenn Sie wissen oder vermuten, dass ein bestimmtes Datenelement benötigt wird, können Sie durch Prefetching den Cache vorab mit diesen Daten füllen, was zu einer schnelleren Benutzererfahrung führt.
Es gibt verschiedene Prefetching-Muster
In diesem Leitfaden werden wir uns die ersten drei ansehen, während die vierte ausführlich im Leitfaden Server-Rendering & Hydration und im Leitfaden Erweitertes Server-Rendering behandelt wird.
Ein spezieller Anwendungsfall für Prefetching ist die Vermeidung von Request Waterfalls. Eine ausführliche Hintergrundinformation und Erklärung dazu finden Sie im Leitfaden Performance & Request Waterfalls.
Bevor wir uns den verschiedenen spezifischen Prefetch-Mustern zuwenden, 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 prefetchen.
Eine einfache Form des Prefetching ist, dies zu tun, wenn der Benutzer mit etwas interagiert. In diesem Beispiel verwenden wir queryClient.prefetchQuery, um ein Prefetching 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 Nachkomponenten eine bestimmte Dateneinheit benötigt, wir diese aber erst rendern können, wenn eine andere Query das Laden abgeschlossen hat. Nehmen wir ein Beispiel aus dem Request Waterfall-Leitfaden zur Erklärung.
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 zu glätten und die Leistung zu verbessern, darin, die Query getArticleCommentsById in die übergeordnete Komponente zu verschieben und das Ergebnis als Prop weiterzugeben. Aber was ist, wenn dies nicht machbar oder wünschenswert ist, zum Beispiel wenn die Komponenten nicht zusammenhängen und mehrere Ebenen dazwischen liegen?
In diesem Fall können wir stattdessen die Query in der übergeordneten Komponente vorfetchen. Der einfachste Weg, dies zu tun, ist die Verwendung einer Query, 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 prefetchen 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 für das Prefetching nicht verwenden, da dies das Prefetching erst nach Abschluss dersuspensful Query starten würde. Für dieses Szenario können Sie die Hooks usePrefetchQuery oder usePrefetchInfiniteQuery verwenden, die in der Bibliothek verfügbar sind.
Sie können jetzt useSuspenseQuery in der Komponente verwenden, die die Daten tatsächlich benötigt. Sie möchten diese spätere Komponente *vielleicht* in ihre eigene <Suspense>-Grenze einwickeln, damit die "sekundäre" Query, die wir vorfetchen, 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 weitere Möglichkeit ist das Prefetching innerhalb der Query-Funktion. Dies ist sinnvoll, wenn Sie wissen, dass jedes Mal, wenn ein Artikel abgerufen wird, höchstwahrscheinlich auch Kommentare benötigt werden. Hierfür 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 Query abgeschlossen ist, was möglicherweise nicht das ist, was Sie wollen.
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 Query während des Komponentenlebenszyklus vorzufetchen. Wählen Sie diejenige, die am besten zu Ihrer Situation passt.
Schauen wir uns als Nächstes einen etwas fortgeschritteneren Fall an.
Manchmal möchten wir bedingt prefetchen, basierend auf dem Ergebnis eines anderen Abrufs. 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 = React.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 = React.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 dort 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 so umstrukturieren können, dass getFeed() bei Bedarf auch die Daten von getGraphDataById() zurückgibt, gibt es keinen Weg, den Waterfall getFeed->getGraphDataById zu beseitigen. Aber durch bedingtes Prefetching können wir zumindest den Code und die Daten parallel laden. Genau wie oben beschrieben, gibt es mehrere Möglichkeiten, dies zu tun, aber für dieses Beispiel werden wir es in der Query-Funktion 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, anstatt im JS für <GraphFeedItem>. Sie müssen also von Fall zu Fall entscheiden, was der beste Performance-Kompromiss ist. Wenn GraphFeedItem wahrscheinlich ist, ist es wahrscheinlich sinnvoll, den Code in die übergeordnete Komponente aufzunehmen. Wenn sie extrem selten vorkommen, wahrscheinlich nicht.
Da das Datenabrufen im Komponentenbaum selbst leicht zu Request Waterfalls führen kann und die verschiedenen Lösungen dafür mit zunehmender Komplexität in der Anwendung umständlich werden können, ist die Integration des Prefetching auf Router-Ebene eine attraktive Möglichkeit.
Bei diesem Ansatz deklarieren Sie für jede *Route* explizit, welche Daten für diesen Komponentenbaum vorab benötigt werden. Da das Server-Rendering traditionell alle Daten vor dem Beginn des Renderns geladen haben muss, war dies lange Zeit der dominierende Ansatz für SSR-Apps. Dies ist immer noch ein gängiger Ansatz, und Sie können mehr darüber im Leitfaden Server-Rendering & Hydration lesen.
Konzentrieren wir uns vorerst auf den Client-seitigen Fall und betrachten ein Beispiel, wie dies mit Tanstack Router funktionieren kann. Diese Beispiele lassen viel Setup 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 Sie das Rendern der Route *blockieren*, bis alle Daten vorhanden sind, oder ob Sie ein Prefetching starten, aber nicht auf das Ergebnis warten. So 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 so, dass sie erst gerendert wird, wenn die Artikeldaten geladen sind. Außerdem starten wir das Prefetching von Kommentaren so schnell wie möglich, aber blockieren das Rendern der Route nicht, 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!',
})
Eine Integration mit anderen Routern ist ebenfalls möglich. Sehen Sie sich react-router für eine weitere Demonstration an.
Wenn Sie die Daten für Ihre Query bereits synchron verfügbar haben, müssen Sie sie nicht vorab abrufen. Sie können einfach die Methode setQueryData des Query Clients verwenden, um das zwischengespeicherte Ergebnis einer Query direkt nach Schlüssel hinzuzufügen oder zu aktualisieren.
queryClient.setQueryData(['todos'], todos)
queryClient.setQueryData(['todos'], todos)
Für eine eingehende Behandlung, wie Sie Daten vor dem Abruf in Ihren Query-Cache bekommen, schauen Sie sich #17: Seeding the Query Cache aus den Community Resources an.
Die Integration mit serverseitigen Routern und Frameworks ist sehr ähnlich zu dem, was wir gerade gesehen haben, mit der zusätzlichen Anforderung, dass die Daten vom Server zum Client übergeben werden müssen, um dort in den Cache geladen zu werden. Um zu lernen, wie das geht, fahren Sie mit dem Leitfaden Server-Rendering & Hydration fort.