Skip to content

Commit

Permalink
Fill out infinite queries usage guide
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Jan 20, 2025
1 parent b873043 commit 4e9b7cf
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 16 deletions.
105 changes: 93 additions & 12 deletions docs/rtk-query/usage/infinite-queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ description: 'RTK Query > Usage > Infinite Queries: fetching many data pages fro

## Overview

Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern.
Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is a common UI pattern.

RTK Query supports this use case via "infinite query" endpoints. Infinite Query endpoints are similar to standard query endpoints, in that they fetch data and cache the results. However, infinite query endpoints have the ability to fetch "next" and "previous" pages, and contain all related fetched pages in a single cache entry.

Expand All @@ -24,17 +24,17 @@ RTK Query's support for infinite queries is modeled after [React Query's infinit

With standard query endpoints:

- You specify the "query arg" value, which is passed to either the `query` function or the `queryFn` function that will calculate the desired URL or do the actual fetching
- You specify the "query arg" value, which is passed to the `query` or `queryFn` function that will calculate the desired URL or do the actual fetching
- The query arg is also serialized to generate the unique internal key for this specific cache entry
- The single response value is directly stored as the `data` field in the cache entry

Infinite queries work similarly, but have a couple additional layers:

- You still specify a "query arg", which is still used to generate the unique cache key for this specific cache entry
- However, there is a separation between the "query arg" used for the cache key, and the "page param" used to fetch a specific page. This means the page param is what will be passed to your `query` or `queryFn` methods.
- The `data` field in the cache entry stores a `{pages: Array<DataType>, pageParams: PageParam[]}` structure that contains _all_ of the fetched page results and their corresponding page params used to fetch them.
- The `data` field in the cache entry stores a `{pages: DataType[], pageParams: PageParam[]}` structure that contains _all_ of the fetched page results and their corresponding page params used to fetch them.

For example, a Pokemon API endpoint might have a string query arg like `"fire"`, but use a page number as the param to determine which page to fetch out of the results. The resulting cache data might look like this:
For example, a Pokemon API endpoint might have a string query arg like `"fire"`, but use a page number as the param to determine which page to fetch out of the results. For a query like `useGetPokemonInfiniteQuery('fire')`, the resulting cache data might look like this:

```ts no-transpile
{
Expand All @@ -61,7 +61,7 @@ This structure allows flexibility in how your UI chooses to render the data (sho

## Defining Infinite Query Endpoints

Infinite query endpoints are defined by returning an object inside the `endpoints` section of `createApi`, and defining the fields using the `build.infiniteQuery()` method. They are an extension of standard query endpoints - you can specify [the same options as standard queries](./queries.mdx#defining-query-endpoints) (providing either `query` or `queryFn`, customizing with `transformResponse`, lifecycles with `onCacheEntryAdded` and `onQueryStarted`, etc). However, they also require an additional `infiniteQueryOptions` field to specify the infinite query behavior.
Infinite query endpoints are defined by returning an object inside the `endpoints` section of `createApi`, and defining the fields using the `build.infiniteQuery()` method. They are an extension of standard query endpoints - you can specify [the same options as standard queries](./queries.mdx#defining-query-endpoints) (providing either `query` or `queryFn`, customizing with `transformResponse`, lifecycles with `onCacheEntryAdded` and `onQueryStarted`, defining tags, etc). However, they also require an additional `infiniteQueryOptions` field to specify the infinite query behavior.

With TypeScript, you must supply 3 generic arguments: `build.infiniteQuery<ResultType, QueryArg, PageParam>`, where `ResultType` is the contents of a single page, `QueryArg` is the type passed in as the cache key, and `PageParam` is the value that will be passed to `query/queryFn` to make the rest. If there is no argument, use `void` for the arg type instead.

Expand All @@ -75,7 +75,7 @@ The `infiniteQueryOptions` field includes:
- `getPreviousPageParam`: an optional callback that will be used to calculate the previous page param, if you try to fetch backwards.

Both `initialPageParam` and `getNextPageParam` are required, to
ensure the infinite query can properly fetch the next page of data.Also, `initialPageParam` may be specified when using the endpoint, to override the default value. `maxPages` and `getPreviousPageParam` are both optional.
ensure the infinite query can properly fetch the next page of data.Also, `initialPageParam` may be specified when using the endpoint, to override the default value for a first fetch. `maxPages` and `getPreviousPageParam` are both optional.

### Page Param Functions

Expand All @@ -84,15 +84,17 @@ ensure the infinite query can properly fetch the next page of data.Also, `initia
```ts
export type PageParamFunction<DataType, PageParam> = (
currentPage: DataType,
allPages: Array<DataType>,
allPages: DataType[],
currentPageParam: PageParam,
allPageParams: Array<PageParam>,
allPageParams: PageParam[],
) => PageParam | undefined | null
```
This enables a number of possible infinite query use cases, including cursor-based and limit+offset-based queries.
A page param can be any value at all: numbers, strings, objects, arrays, etc. Since the existing page param values are stored in Redux state, you should still treat those immutably. For example, if you had a param structure like `{page: Number, filters: Filters}`, incrementing the page would look like `return {...currentPageParam, page: currentPageParam.page + 1}`.
The "current" arguments will be either the last page for `getNext`, or the first page for `getPrevious`.
Since both actual page contents and page params are passed in, you can calculate new page params based on any of those. This enables a number of possible infinite query use cases, including cursor-based and limit+offset-based queries.
The "current" arguments will be either the last page for `getNextPageParam`, or the first page for `getPreviousPageParam`.
If there is no possible page to fetch in that direction, the callback should return `undefined`.
Expand Down Expand Up @@ -134,7 +136,7 @@ const pokemonApi = createApi({

## Performing Infinite Queries with React Hooks

[Similar to query endpoints](./queries.mdx#performing-queries-with-react-hooks), RTK Query will automatically generate React hooks for infinite query endpoints based on the name of the endpoint. An endpoint field with `getPokemon: build.infiniteQuery()` will generate a hook named `useGetPokemonInfiniteQuery`.
[Similar to query endpoints](./queries.mdx#performing-queries-with-react-hooks), RTK Query will automatically generate React hooks for infinite query endpoints based on the name of the endpoint. An endpoint field with `getPokemon: build.infiniteQuery()` will generate a hook named `useGetPokemonInfiniteQuery`, as well as a generically-named hook attached to the endpoint, like `api.endpoints.getPokemon.useInfiniteQuery`.

### Hook Types

Expand All @@ -155,7 +157,7 @@ The query hooks expect two parameters: `(queryArg?, queryOptions?)`.

The `queryOptions` object accepts [all the same parameters as `useQuery`](./queries.mdx#query-hook-options), including `skip`, `selectFromResult`, and refetching/polling options.

Unlike normal query hooks, your `query` or `queryFn` callbacks will receive a "page param" value to generate the URL or make the request. By default, the `initialPageParam` value specified in the endpoint will be used to make the first request, and then your `getNext/PreviousPageParam` callbacks will be used to calculate further page params as you fetch forwards or backwards.
Unlike normal query hooks, your `query` or `queryFn` callbacks will receive a "page param" value to generate the URL or make the request, instead of the "query arg" that was passed to the hook. By default, the `initialPageParam` value specified in the endpoint will be used to make the first request, and then your `getNext/PreviousPageParam` callbacks will be used to calculate further page params as you fetch forwards or backwards.

If you want to start from a different page param, you may override the `initialPageParam` by passing it as part of the hook options:

Expand All @@ -166,3 +168,82 @@ const { data } = useGetPokemonInfiniteQuery('fire', {
```

The next and previous page params will still be calculated as needed.

### Frequently Used Query Hook Return Values

Infinite query hooks return [the same result object as normal query hooks](./queries.mdx#frequently-used-query-hook-return-values), but with [a few additional fields specific to infinite queries](../api/created-api/hooks.mdx#useinfinitequery-signature) and a different structure for `data` and `currentData`.

- `data` / `currentData`: These contain the same "latest successful" and "latest for current arg" results as normal queries, but the value is the `{pages, pageParams}` infinite query object with all fetched pages instead of a single response value.
- `hasNextPage` / `hasPreviousPage`: When true, indicates that there _should_ be another page available to fetch in that direction. This is calculated by calling `getNext/PreviousPageParam` with the latest fetched pages.
- `isFetchingNext/PreviousPage`: When true, indicates that the current `isFetching` flag represents a fetch in that direction.
- `isFetchNext/PreviousPageError`: When true, indicates that the current `isError` flag represents an error for a failed fetch in that direction
- `fetchNext/PreviousPage`: methods that will trigger a fetch for another page in that direction.

In most cases, you will probably read `data` and either `isLoading` or `isFetching` in order to render your UI. You will also want to use the `fetchNext/PreviousPage` methods to trigger fetching additional pages.

### Infinite Query Hook Usage Example

Here is an example of a typical infinite query endpoint definition, and hook usage in a component:

```tsx no-transpile
type Pokemon = {
id: string
name: string
}

const pokemonApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemon: builder.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
lastPageParam + 1,
},
query(pageParam) {
return `https://example.com/listItems?page=${pageParam}`
},
}),
}),
})

function PokemonList({ pokemonType }: { pokemonType: string }) {
const { data, isFetching, fetchNextPage, fetchPreviousPage, refetch } =
pokemonApi.useGetPokemonInfiniteQuery(pokemonType)

const handleNextPage = async () => {
await fetchNextPage()
}

const handleRefetch = async () => {
await refetch()
}

const allResults = data?.pages.flat() ?? []

return (
<div>
<div>Type: {pokemonType}</div>
<div>
{allResults.map((pokemon, i: number | null | undefined) => (
<div key={i}>{pokemon.name}</div>
))}
</div>
<button onClick={() => handleNextPage()}>Fetch More</button>
<button onClick={() => handleRefetch()}>Refetch</button>
</div>
)
}
```

In this example, the server returns an array of Pokemon as the response for each individual page. This component shows the results as a single list. Since the `data` field itself has a `pages` array of all responses, the component needs to flatten the pages into a single array to render that list. Alternately, it could map over the pages and show them in a paginated format.

Similarly, this example relies on manual user clicks on a "Fetch More" button to trigger fetching the next page, but could automatically call `fetchNextPage` based on things like an `IntersectionObserver`, a list component triggering some kind of "end of the list" event, or other similar indicators.

The endpoint itself only defines `getNextPageParam`, so this example doesn't support fetching backwards, but that can be provided in cases where backwards fetching makes sense. The page param here is a simple incremented number, but the page param

## Limiting Cache Entry Size

All fetched pages for a given query arg are stored in the `pages` array in that cache entry. By default, there is no limit to the number of stored pages - if you call `fetchNextPage()` 1000 times, `data.pages` will have 1000 pages stored.

If you need to limit the number of stored pages (for reasons like memory usage), you can supply a `maxPages` option as part of the endpoint. If provided, fetching a page when already at the max will automatically drop the last page in the opposite direction. For example, with `maxPages: 3` and a cached page params of `[1, 2, 3]`, calling `fetchNextPage()` would result in page `1` being dropped and the new cached pages being `[2, 3, 4]`. From there, calling `fetchNextPage()` would result in `[3, 4, 5]`, or calling `fetchPreviousPage()` would go back to `[1, 2, 3]`.
6 changes: 3 additions & 3 deletions docs/rtk-query/usage/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ description: 'RTK Query > Usage > Queries: fetching data from a server'

## Overview

This is the most common use case for RTK Query. A query operation can be performed with any data fetching library of your choice, but the general recommendation is that you only use queries for requests that retrieve data. For anything that alters data on the server or will possibly invalidate the cache, you should use a [Mutation](./mutations).
Queries are operations that fetch data from the server and cache it within the client. This is the most common use case for RTK Query. A query operation can be performed with any data fetching library of your choice, but the general recommendation is that you only use queries for requests that retrieve data. For anything that alters data on the server or will possibly invalidate the cache, you should use a [Mutation](./mutations).

By default, RTK Query ships with [`fetchBaseQuery`](../api/fetchBaseQuery), which is a lightweight [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) wrapper that automatically handles request headers and response parsing in a manner similar to common libraries like `axios`. See [Customizing Queries](./customizing-queries) if `fetchBaseQuery` does not handle your requirements.
A query can cache the status and result of any async/promise method. Since the most common type of query is an HTTP request, RTK Query ships with [`fetchBaseQuery`](../api/fetchBaseQuery), which is a lightweight [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) wrapper that automatically handles request headers and response parsing in a manner similar to common libraries like `axios`. See [Customizing Queries](./customizing-queries) if `fetchBaseQuery` does not handle your requirements.

:::info

Expand Down Expand Up @@ -105,7 +105,7 @@ const api = createApi({

If you're using React Hooks, RTK Query does a few additional things for you. The primary benefit is that you get a render-optimized hook that allows you to have 'background fetching' as well as [derived booleans](#frequently-used-query-hook-return-values) for convenience.

Hooks are automatically generated based on the name of the `endpoint` in the service definition. An endpoint field with `getPost: builder.query()` will generate a hook named `useGetPostQuery`.
Hooks are automatically generated based on the name of the `endpoint` in the service definition. An endpoint field with `getPost: builder.query()` will generate a hook named `useGetPostQuery`, as well as a generically-named hook attached to the endpoint, like `api.endpoints.getPost.useQuery`.

### Hook types

Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/query/tests/buildHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1789,7 +1789,7 @@ describe('hooks tests', () => {
))}
</div>
<button data-testid="prevPage" onClick={() => handlePreviousPage()}>
nextPage
previousPage
</button>
<button data-testid="nextPage" onClick={() => handleNextPage()}>
nextPage
Expand Down

0 comments on commit 4e9b7cf

Please sign in to comment.