Framework
Version

Prefetching & Router-Integration

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

  1. In Event-Handlern
  2. In Komponenten
  3. Über Router-Integration
  4. Während Server-Rendering (eine weitere Form der Router-Integration)

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.

prefetchQuery & prefetchInfiniteQuery

Bevor wir uns mit den verschiedenen spezifischen Prefetch-Mustern befassen, werfen wir einen Blick auf die Funktionen prefetchQuery und prefetchInfiniteQuery. Zuerst ein paar Grundlagen

  • Standardmäßig verwenden diese Funktionen die für den queryClient konfigurierte Standard- staleTime, um festzustellen, ob vorhandene Daten im Cache frisch sind oder erneut abgerufen werden müssen.
  • Sie können auch eine spezifische staleTime angeben, wie folgt: prefetchQuery({ queryKey: ['todos'], queryFn: fn, staleTime: 5000 })
    • Diese staleTime wird nur für das Prefetching verwendet. Sie müssen sie auch für jeden useQuery-Aufruf festlegen.
    • Wenn Sie die staleTime ignorieren und stattdessen immer Daten zurückgeben möchten, wenn diese im Cache verfügbar sind, können Sie die Funktion ensureQueryData verwenden.
    • Tipp: Wenn Sie auf dem Server prefetching betreiben, setzen Sie eine Standard- staleTime, die höher als 0 ist, für diesen queryClient, um zu vermeiden, dass Sie eine spezifische staleTime für jeden Prefetch-Aufruf übergeben müssen.
  • Wenn keine Instanzen von useQuery für eine vorgefetchte Abfrage erscheinen, wird sie nach der in gcTime angegebenen Zeit gelöscht und gesammelt.
  • Diese Funktionen geben Promise<void> zurück und liefern daher niemals Abfragedaten. Wenn Sie dies benötigen, verwenden Sie stattdessen fetchQuery/fetchInfiniteQuery.
  • Die Prefetch-Funktionen werfen niemals Fehler, da sie normalerweise versuchen, in einem useQuery erneut zu fetchen, was ein schöner, fehlerverzeihender Fallback ist. Wenn Sie Fehler abfangen müssen, verwenden Sie stattdessen fetchQuery/fetchInfiniteQuery.

So verwenden Sie prefetchQuery

tsx
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.

tsx
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.

Prefetching in Event-Handlern

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.

tsx
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 in Komponenten

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

tsx
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.

tsx
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.

tsx
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.

tsx
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.

tsx
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.

  • Prefetch vor einer Suspense-Grenze mit den Hooks usePrefetchQuery oder usePrefetchInfiniteQuery
  • Verwenden Sie useQuery oder useSuspenseQueries und ignorieren Sie das Ergebnis.
  • Prefetch innerhalb der Abfragefunktion
  • Prefetch in einem Effekt

Als Nächstes betrachten wir einen etwas fortgeschritteneren Fall.

Abhängige Abfragen & Code Splitting

Manchmal möchten wir bedingt prefetchten, basierend auf dem Ergebnis eines anderen Fetches. Betrachten Sie dieses Beispiel aus dem Leitfaden Performance & Request Waterfalls.

tsx
// 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.

tsx
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.

Router-Integration

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.

tsx
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.

Manuelles Vorfüllen einer Abfrage

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.

tsx
queryClient.setQueryData(['todos'], todos)
queryClient.setQueryData(['todos'], todos)

Weiterführende Lektüre

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.