Framework
Version

Prefetching & Router-Integration

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

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

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.

prefetchQuery & prefetchInfiniteQuery

Bevor wir uns den verschiedenen spezifischen Prefetch-Mustern zuwenden, 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 zu bestimmen, ob vorhandene Daten im Cache frisch sind oder erneut abgerufen werden müssen.
  • Sie können auch eine spezifische staleTime wie folgt übergeben: 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 sie im Cache verfügbar sind, können Sie die Funktion ensureQueryData verwenden.
    • Tipp: Wenn Sie auf dem Server prefetchen, setzen Sie eine Standard- staleTime höher als 0 für diesen queryClient, um zu vermeiden, dass Sie jedem Prefetch-Aufruf eine spezifische staleTime übergeben müssen.
  • Wenn keine Instanzen von useQuery für eine vorgefetchte Query erscheinen, wird sie nach der in gcTime angegebenen Zeit gelöscht und gesammelt.
  • Diese Funktionen geben Promise<void> zurück und geben daher niemals Query-Daten zurück. Wenn Sie das benötigen, verwenden Sie stattdessen fetchQuery/fetchInfiniteQuery.
  • Die Prefetch-Funktionen werfen niemals Fehler, da sie normalerweise versuchen, erneut in einer useQuery abzurufen, 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 prefetchen.

Prefetching in Event-Handlern

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.

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

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

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

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

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 Query abgeschlossen ist, was möglicherweise nicht das ist, was Sie wollen.

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 Query während des Komponentenlebenszyklus vorzufetchen. Wählen Sie diejenige, die am besten zu Ihrer Situation passt.

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

Schauen wir uns als Nächstes einen etwas fortgeschritteneren Fall an.

Abhängige Queries & Code Splitting

Manchmal möchten wir bedingt prefetchen, basierend auf dem Ergebnis eines anderen Abrufs. 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 = 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.

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

Router-Integration

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.

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!',
})

Eine Integration mit anderen Routern ist ebenfalls möglich. Sehen Sie sich react-router für eine weitere Demonstration an.

Manuelles Vorbereiten einer Query

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.

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

Weiterführende Lektüre

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.