From 0f4f87a694786f106b7b19ddeecc04c13933f4d0 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 22 Feb 2025 14:29:25 -0500 Subject: [PATCH 1/7] Update example app RTK build --- examples/query/react/infinite-queries/package.json | 2 +- examples/query/react/infinite-queries/yarn.lock | 10 +++++----- yarn.lock | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/query/react/infinite-queries/package.json b/examples/query/react/infinite-queries/package.json index 8f79449dda..2c38bbf514 100644 --- a/examples/query/react/infinite-queries/package.json +++ b/examples/query/react/infinite-queries/package.json @@ -15,7 +15,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz", + "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.13.1", diff --git a/examples/query/react/infinite-queries/yarn.lock b/examples/query/react/infinite-queries/yarn.lock index 6991374198..3b0ea688b4 100644 --- a/examples/query/react/infinite-queries/yarn.lock +++ b/examples/query/react/infinite-queries/yarn.lock @@ -1743,9 +1743,9 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz": - version: 2.5.0 - resolution: "@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz" +"@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz": + version: 2.5.1 + resolution: "@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz" dependencies: immer: "npm:^10.0.3" redux: "npm:^5.0.1" @@ -1759,7 +1759,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10/9bf78a12a14a745cfb2eef7e8509149652dba734b66012ac42449722c72bf3709b37e8dba1814635ab7baef04c286be003b4df7f7098f17e059ad3e449576f2f + checksum: 10/302a0bfb1a4ba3433bf6c4565e8bbac7163869f8b651cc1fbb94efa90f80344d09a47fbc9f86f2abc1269f4a16465d610cd25f92b934fde9292a125fe630b12c languageName: node linkType: hard @@ -7433,7 +7433,7 @@ __metadata: version: 0.0.0-use.local resolution: "vite-template-redux@workspace:." dependencies: - "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz" + "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz" "@testing-library/dom": "npm:^9.3.4" "@testing-library/jest-dom": "npm:^6.2.0" "@testing-library/react": "npm:^14.1.2" diff --git a/yarn.lock b/yarn.lock index 0bc9dc916f..8eb846aabc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8042,9 +8042,9 @@ __metadata: languageName: unknown linkType: soft -"@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz": - version: 2.5.0 - resolution: "@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz" +"@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz": + version: 2.5.1 + resolution: "@reduxjs/toolkit@https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz" dependencies: immer: "npm:^10.0.3" redux: "npm:^5.0.1" @@ -8058,7 +8058,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10/9bf78a12a14a745cfb2eef7e8509149652dba734b66012ac42449722c72bf3709b37e8dba1814635ab7baef04c286be003b4df7f7098f17e059ad3e449576f2f + checksum: 10/302a0bfb1a4ba3433bf6c4565e8bbac7163869f8b651cc1fbb94efa90f80344d09a47fbc9f86f2abc1269f4a16465d610cd25f92b934fde9292a125fe630b12c languageName: node linkType: hard @@ -34081,7 +34081,7 @@ __metadata: version: 0.0.0-use.local resolution: "vite-template-redux@workspace:examples/query/react/infinite-queries" dependencies: - "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/e18b966e/@reduxjs/toolkit/_pkg.tgz" + "@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/2aca4f24/@reduxjs/toolkit/_pkg.tgz" "@testing-library/dom": "npm:^9.3.4" "@testing-library/jest-dom": "npm:^6.2.0" "@testing-library/react": "npm:^14.1.2" From 425087013e72088a0f7b7cb13a62d966957883d6 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 22 Feb 2025 15:24:44 -0500 Subject: [PATCH 2/7] Use "build" consistently instead of "builder" --- .../api/created-api/code-splitting.mdx | 8 ++--- docs/rtk-query/api/fetchBaseQuery.mdx | 30 +++++++++---------- docs/rtk-query/overview.md | 4 +-- docs/rtk-query/usage-with-typescript.mdx | 12 ++++---- docs/rtk-query/usage/cache-behavior.mdx | 24 +++++++-------- docs/rtk-query/usage/customizing-queries.mdx | 18 +++++------ .../usage/migrating-to-rtk-query.mdx | 4 +-- docs/rtk-query/usage/pagination.mdx | 4 +-- docs/rtk-query/usage/queries.mdx | 4 +-- .../react/advanced/src/services/pokemon.ts | 4 +-- .../src/app/services/auth.ts | 6 ++-- .../authentication/src/app/services/auth.ts | 6 ++-- .../query/react/basic/src/services/pokemon.ts | 4 +-- .../src/services/pokemon.ts | 4 +-- .../deduping-queries/src/services/pokemon.ts | 4 +-- .../infiniteScrollApi.ts | 6 ++-- .../limit-offset/infiniteScrollApi.ts | 11 +++---- .../infiniteScrollApi.ts | 4 +-- .../react/polling/src/services/pokemon.ts | 4 +-- .../query/react/with-apiprovider/src/App.tsx | 4 +-- .../src/query/tests/infiniteQueries.test.ts | 8 ++--- 21 files changed, 85 insertions(+), 88 deletions(-) diff --git a/docs/rtk-query/api/created-api/code-splitting.mdx b/docs/rtk-query/api/created-api/code-splitting.mdx index 7af418413d..ff3ce318c0 100644 --- a/docs/rtk-query/api/created-api/code-splitting.mdx +++ b/docs/rtk-query/api/created-api/code-splitting.mdx @@ -76,18 +76,18 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/' }), - endpoints: (builder) => ({ - getUserByUserId: builder.query({ + endpoints: (build) => ({ + getUserByUserId: build.query({ query() { return '' }, }), - patchUserByUserId: builder.mutation({ + patchUserByUserId: build.mutation({ query() { return '' }, }), - getUsers: builder.query({ + getUsers: build.query({ query() { return '' }, diff --git a/docs/rtk-query/api/fetchBaseQuery.mdx b/docs/rtk-query/api/fetchBaseQuery.mdx index 8f88b444ce..1cbaee240a 100644 --- a/docs/rtk-query/api/fetchBaseQuery.mdx +++ b/docs/rtk-query/api/fetchBaseQuery.mdx @@ -26,12 +26,12 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' export const pokemonApi = createApi({ // Set the baseUrl for every endpoint below baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ // Will make a request like https://pokeapi.co/api/v2/pokemon/bulbasaur query: (name: string) => `pokemon/${name}`, }), - updatePokemon: builder.mutation({ + updatePokemon: build.mutation({ query: ({ name, patch }) => ({ url: `pokemon/${name}`, // When performing a mutation, you typically use a method of @@ -260,8 +260,8 @@ By default, `fetchBaseQuery` assumes that every request you make will be `json`, ```ts no-transpile // omitted - endpoints: (builder) => ({ - updateUser: builder.query({ + endpoints: (build) => ({ + updateUser: build.query({ query: (user: Record) => ({ url: `users`, method: 'PUT', @@ -274,8 +274,8 @@ By default, `fetchBaseQuery` assumes that every request you make will be `json`, ```ts no-transpile // omitted - endpoints: (builder) => ({ - updateUser: builder.query({ + endpoints: (build) => ({ + updateUser: build.query({ query: (user: Record) => ({ url: `users`, method: 'PUT', @@ -296,8 +296,8 @@ By default, `fetchBaseQuery` assumes that every request you make will be `json`, ```ts no-transpile // omitted - endpoints: (builder) => ({ - updateUser: builder.query({ + endpoints: (build) => ({ + updateUser: build.query({ query: (user: Record) => ({ url: `users`, // Assuming no `paramsSerializer` is specified, the user object is automatically converted @@ -328,8 +328,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' export const customApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), - endpoints: (builder) => ({ - getUsers: builder.query({ + endpoints: (build) => ({ + getUsers: build.query({ query: () => ({ url: `users`, // This is the same as passing 'text' @@ -365,8 +365,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' export const customApi = createApi({ // Set the baseUrl for every endpoint below baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), - endpoints: (builder) => ({ - getUsers: builder.query({ + endpoints: (build) => ({ + getUsers: build.query({ query: () => ({ url: `users`, // Example: we have a backend API always returns a 200, @@ -389,8 +389,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' export const api = createApi({ // Set a default timeout of 10 seconds baseQuery: fetchBaseQuery({ baseUrl: '/api/', timeout: 10000 }), - endpoints: (builder) => ({ - getUsers: builder.query({ + endpoints: (build) => ({ + getUsers: build.query({ query: () => ({ url: `users`, // Example: we know the users endpoint is _really fast_ because it's always cached. diff --git a/docs/rtk-query/overview.md b/docs/rtk-query/overview.md index 6ae2256660..bd33648286 100644 --- a/docs/rtk-query/overview.md +++ b/docs/rtk-query/overview.md @@ -121,8 +121,8 @@ import type { Pokemon } from './types' export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name) => `pokemon/${name}`, }), }), diff --git a/docs/rtk-query/usage-with-typescript.mdx b/docs/rtk-query/usage-with-typescript.mdx index 5497f0b309..a9f67bbbd9 100644 --- a/docs/rtk-query/usage-with-typescript.mdx +++ b/docs/rtk-query/usage-with-typescript.mdx @@ -51,8 +51,8 @@ import type { Pokemon } from './types' export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name) => `pokemon/${name}`, }), }), @@ -78,8 +78,8 @@ import type { Pokemon } from './types' export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name) => `pokemon/${name}`, }), }), @@ -178,8 +178,8 @@ const simpleBaseQuery: BaseQueryFn< const api = createApi({ baseQuery: simpleBaseQuery, - endpoints: (builder) => ({ - getSupport: builder.query({ + endpoints: (build) => ({ + getSupport: build.query({ query: () => 'support me', extraOptions: { shout: true, diff --git a/docs/rtk-query/usage/cache-behavior.mdx b/docs/rtk-query/usage/cache-behavior.mdx index 97ff3d333d..84b46a2048 100644 --- a/docs/rtk-query/usage/cache-behavior.mdx +++ b/docs/rtk-query/usage/cache-behavior.mdx @@ -100,8 +100,8 @@ export const api = createApi({ // global configuration for the api keepUnusedDataFor: 30, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, // highlight-start // configuration for an individual endpoint, overriding the api setting @@ -189,8 +189,8 @@ export const api = createApi({ // global configuration for the api refetchOnMountOrArgChange: 30, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, }), }), @@ -241,8 +241,8 @@ export const api = createApi({ // global configuration for the api refetchOnFocus: true, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, }), }), @@ -266,8 +266,8 @@ export const api = createApi({ // global configuration for the api refetchOnFocus: true, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, }), }), @@ -320,8 +320,8 @@ export const api = createApi({ // global configuration for the api refetchOnReconnect: true, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, }), }), @@ -345,8 +345,8 @@ export const api = createApi({ // global configuration for the api refetchOnReconnect: true, // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => `posts`, }), }), diff --git a/docs/rtk-query/usage/customizing-queries.mdx b/docs/rtk-query/usage/customizing-queries.mdx index 7e9629075b..e78d12dc80 100644 --- a/docs/rtk-query/usage/customizing-queries.mdx +++ b/docs/rtk-query/usage/customizing-queries.mdx @@ -446,8 +446,8 @@ export const api = createApi({ baseUrl: 'https://graphqlzero.almansi.me/api', }), // highlight-end - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => ({ body: gql` query { @@ -462,7 +462,7 @@ export const api = createApi({ }), transformResponse: (response) => response.posts.data, }), - getPost: builder.query({ + getPost: build.query({ query: (id) => ({ body: gql` query { @@ -823,8 +823,8 @@ const dynamicBaseQuery: BaseQueryFn< export const api = createApi({ baseQuery: dynamicBaseQuery, - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => 'posts', }), }), @@ -862,8 +862,8 @@ export const api = createApi({ baseQuery: graphqlBaseQuery({ baseUrl: '/graphql', }), - endpoints: (builder) => ({ - getPosts: builder.query({ + endpoints: (build) => ({ + getPosts: build.query({ query: () => ({ body: gql` query { @@ -959,8 +959,8 @@ import { supabase } from './supabaseApi' export const supabaseApi = createApi({ reducerPath: 'supabaseApi', baseQuery: fakeBaseQuery(), - endpoints: (builder) => ({ - getBlogs: builder.query({ + endpoints: (build) => ({ + getBlogs: build.query({ queryFn: async () => { // Supabase conveniently already has `data` and `error` fields const { data, error } = await supabase.from('blogs').select() diff --git a/docs/rtk-query/usage/migrating-to-rtk-query.mdx b/docs/rtk-query/usage/migrating-to-rtk-query.mdx index 88cff6a83d..2e32c7f556 100644 --- a/docs/rtk-query/usage/migrating-to-rtk-query.mdx +++ b/docs/rtk-query/usage/migrating-to-rtk-query.mdx @@ -375,8 +375,8 @@ interface Pokemon {} export const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name) => `pokemon/${name}`, }), }), diff --git a/docs/rtk-query/usage/pagination.mdx b/docs/rtk-query/usage/pagination.mdx index a9241ad9fb..2c0e2fa80f 100644 --- a/docs/rtk-query/usage/pagination.mdx +++ b/docs/rtk-query/usage/pagination.mdx @@ -32,8 +32,8 @@ interface ListResponse { export const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: '/' }), - endpoints: (builder) => ({ - listPosts: builder.query, number | void>({ + endpoints: (build) => ({ + listPosts: build.query, number | void>({ query: (page = 1) => `posts?page=${page}`, }), }), diff --git a/docs/rtk-query/usage/queries.mdx b/docs/rtk-query/usage/queries.mdx index 91775607f8..6f185aedf9 100644 --- a/docs/rtk-query/usage/queries.mdx +++ b/docs/rtk-query/usage/queries.mdx @@ -26,7 +26,7 @@ See [`useQuery`](../api/created-api/hooks.mdx#usequery) for the hook signature a ## Defining Query Endpoints -Query endpoints are defined by returning an object inside the `endpoints` section of `createApi`, and defining the fields using the `builder.query()` method. +Query endpoints are defined by returning an object inside the `endpoints` section of `createApi`, and defining the fields using the `build.query()` method. Query endpoints should define either a `query` callback that constructs the URL (including any URL query params), or [a `queryFn` callback](./customizing-queries.mdx#customizing-queries-with-queryfn) that may do arbitrary async logic and return a result. @@ -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`, as well as a generically-named hook attached to the endpoint, like `api.endpoints.getPost.useQuery`. +Hooks are automatically generated based on the name of the `endpoint` in the service definition. An endpoint field with `getPost: build.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 diff --git a/examples/query/react/advanced/src/services/pokemon.ts b/examples/query/react/advanced/src/services/pokemon.ts index 5ec3234f1b..8ea3dca5f7 100644 --- a/examples/query/react/advanced/src/services/pokemon.ts +++ b/examples/query/react/advanced/src/services/pokemon.ts @@ -4,8 +4,8 @@ export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), tagTypes: [], - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: string) => `pokemon/${name}`, }), }), diff --git a/examples/query/react/authentication-with-extrareducers/src/app/services/auth.ts b/examples/query/react/authentication-with-extrareducers/src/app/services/auth.ts index 94cae9c28f..371e6df347 100644 --- a/examples/query/react/authentication-with-extrareducers/src/app/services/auth.ts +++ b/examples/query/react/authentication-with-extrareducers/src/app/services/auth.ts @@ -28,15 +28,15 @@ export const api = createApi({ return headers }, }), - endpoints: (builder) => ({ - login: builder.mutation({ + endpoints: (build) => ({ + login: build.mutation({ query: (credentials) => ({ url: 'login', method: 'POST', body: credentials, }), }), - protected: builder.mutation<{ message: string }, void>({ + protected: build.mutation<{ message: string }, void>({ query: () => 'protected', }), }), diff --git a/examples/query/react/authentication/src/app/services/auth.ts b/examples/query/react/authentication/src/app/services/auth.ts index 94cae9c28f..371e6df347 100644 --- a/examples/query/react/authentication/src/app/services/auth.ts +++ b/examples/query/react/authentication/src/app/services/auth.ts @@ -28,15 +28,15 @@ export const api = createApi({ return headers }, }), - endpoints: (builder) => ({ - login: builder.mutation({ + endpoints: (build) => ({ + login: build.mutation({ query: (credentials) => ({ url: 'login', method: 'POST', body: credentials, }), }), - protected: builder.mutation<{ message: string }, void>({ + protected: build.mutation<{ message: string }, void>({ query: () => 'protected', }), }), diff --git a/examples/query/react/basic/src/services/pokemon.ts b/examples/query/react/basic/src/services/pokemon.ts index 0a2b88e6bf..14e5f55b3f 100644 --- a/examples/query/react/basic/src/services/pokemon.ts +++ b/examples/query/react/basic/src/services/pokemon.ts @@ -3,8 +3,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export const pokemonApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), tagTypes: [], - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: string) => `pokemon/${name}`, }), }), diff --git a/examples/query/react/conditional-fetching/src/services/pokemon.ts b/examples/query/react/conditional-fetching/src/services/pokemon.ts index fb7cc5c121..d39c9ba120 100644 --- a/examples/query/react/conditional-fetching/src/services/pokemon.ts +++ b/examples/query/react/conditional-fetching/src/services/pokemon.ts @@ -4,8 +4,8 @@ import type { PokemonName } from '../pokemon.data' export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: PokemonName) => `pokemon/${name}`, }), }), diff --git a/examples/query/react/deduping-queries/src/services/pokemon.ts b/examples/query/react/deduping-queries/src/services/pokemon.ts index 67f55e3b9b..3278dcc637 100644 --- a/examples/query/react/deduping-queries/src/services/pokemon.ts +++ b/examples/query/react/deduping-queries/src/services/pokemon.ts @@ -2,8 +2,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export const pokemonApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: string) => `pokemon/${name}`, }), }), diff --git a/examples/query/react/infinite-queries/src/features/bidirectional-cursor-infinite-scroll/infiniteScrollApi.ts b/examples/query/react/infinite-queries/src/features/bidirectional-cursor-infinite-scroll/infiniteScrollApi.ts index ebfbd0fcbd..dc4b68ddab 100644 --- a/examples/query/react/infinite-queries/src/features/bidirectional-cursor-infinite-scroll/infiniteScrollApi.ts +++ b/examples/query/react/infinite-queries/src/features/bidirectional-cursor-infinite-scroll/infiniteScrollApi.ts @@ -16,7 +16,7 @@ type ProjectsCursorPaginated = { } } -interface ProjectsInitialPageParam { +type ProjectsInitialPageParam = { before?: number around?: number after?: number @@ -25,8 +25,8 @@ interface ProjectsInitialPageParam { type QueryParamLimit = number export const apiWithInfiniteScroll = baseApi.injectEndpoints({ - endpoints: builder => ({ - getProjectsBidirectionalCursor: builder.infiniteQuery< + endpoints: build => ({ + getProjectsBidirectionalCursor: build.infiniteQuery< ProjectsCursorPaginated, QueryParamLimit, ProjectsInitialPageParam diff --git a/examples/query/react/infinite-queries/src/features/limit-offset/infiniteScrollApi.ts b/examples/query/react/infinite-queries/src/features/limit-offset/infiniteScrollApi.ts index e093bdca93..97aae393e5 100644 --- a/examples/query/react/infinite-queries/src/features/limit-offset/infiniteScrollApi.ts +++ b/examples/query/react/infinite-queries/src/features/limit-offset/infiniteScrollApi.ts @@ -11,14 +11,14 @@ export type ProjectsResponse = { serverTime: string } -interface ProjectsInitialPageParam { +type ProjectsInitialPageParam = { offset: number limit: number } export const apiWithInfiniteScroll = baseApi.injectEndpoints({ - endpoints: builder => ({ - projectsLimitOffset: builder.infiniteQuery< + endpoints: build => ({ + projectsLimitOffset: build.infiniteQuery< ProjectsResponse, void, ProjectsInitialPageParam @@ -62,10 +62,7 @@ export const apiWithInfiniteScroll = baseApi.injectEndpoints({ }, }, query: ({ pageParam: { offset, limit } }) => { - return { - url: `https://example.com/api/projectsLimitOffset?offset=${offset}&limit=${limit}`, - method: "GET", - } + return `https://example.com/api/projectsLimitOffset?offset=${offset}&limit=${limit}` }, }), }), diff --git a/examples/query/react/infinite-queries/src/features/pagination-infinite-scroll/infiniteScrollApi.ts b/examples/query/react/infinite-queries/src/features/pagination-infinite-scroll/infiniteScrollApi.ts index d1c6891b5a..eaa463f169 100644 --- a/examples/query/react/infinite-queries/src/features/pagination-infinite-scroll/infiniteScrollApi.ts +++ b/examples/query/react/infinite-queries/src/features/pagination-infinite-scroll/infiniteScrollApi.ts @@ -17,8 +17,8 @@ interface ProjectsInitialPageParam { } export const apiWithInfiniteScroll = baseApi.injectEndpoints({ - endpoints: builder => ({ - projectsPaginated: builder.infiniteQuery< + endpoints: build => ({ + projectsPaginated: build.infiniteQuery< ProjectsResponse, void, ProjectsInitialPageParam diff --git a/examples/query/react/polling/src/services/pokemon.ts b/examples/query/react/polling/src/services/pokemon.ts index fb7cc5c121..d39c9ba120 100644 --- a/examples/query/react/polling/src/services/pokemon.ts +++ b/examples/query/react/polling/src/services/pokemon.ts @@ -4,8 +4,8 @@ import type { PokemonName } from '../pokemon.data' export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: PokemonName) => `pokemon/${name}`, }), }), diff --git a/examples/query/react/with-apiprovider/src/App.tsx b/examples/query/react/with-apiprovider/src/App.tsx index f8ded9f1b6..2b91bbbfb1 100644 --- a/examples/query/react/with-apiprovider/src/App.tsx +++ b/examples/query/react/with-apiprovider/src/App.tsx @@ -6,8 +6,8 @@ import { const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2' }), - endpoints: (builder) => ({ - getPokemonByName: builder.query({ + endpoints: (build) => ({ + getPokemonByName: build.query({ query: (name: string) => `pokemon/${name}`, }), }), diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 9d24b611b9..4d952b5492 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -826,8 +826,8 @@ describe('Infinite queries', () => { const pokemonApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getInfinitePokemonWithLifecycles: builder.infiniteQuery< + endpoints: (build) => ({ + getInfinitePokemonWithLifecycles: build.infiniteQuery< Pokemon[], string, number @@ -904,8 +904,8 @@ describe('Infinite queries', () => { type PokemonPage = { items: Pokemon[]; page: number } const pokemonApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (builder) => ({ - getInfinitePokemonWithTransform: builder.infiniteQuery< + endpoints: (build) => ({ + getInfinitePokemonWithTransform: build.infiniteQuery< PokemonPage, string, number From 79a42950f3b6b0fdbd095ef7447326dcf6d795d9 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 22 Feb 2025 15:24:55 -0500 Subject: [PATCH 3/7] Document infinite query patterns --- docs/rtk-query/usage/infinite-queries.mdx | 260 +++++++++++++++++++++- 1 file changed, 258 insertions(+), 2 deletions(-) diff --git a/docs/rtk-query/usage/infinite-queries.mdx b/docs/rtk-query/usage/infinite-queries.mdx index fd1cd5fc10..0209f2e833 100644 --- a/docs/rtk-query/usage/infinite-queries.mdx +++ b/docs/rtk-query/usage/infinite-queries.mdx @@ -31,7 +31,7 @@ With standard query endpoints: 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. Since both are useful for determining what to fetch, your `query` and `queryFn` methods will receive a combined object with `{queryArg, pageParam}` as the first argument, instead of just the `queryArg` by itself. +- However, there is a separation between the "query arg" used for the cache key, and the "page param" used to fetch a specific page. Since both are useful for determining what to fetch, **your `query` and `queryFn` methods will receive a combined object with `{queryArg, pageParam}` as the first argument, instead of just the `queryArg` by itself**. - 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. For a query like `useGetPokemonInfiniteQuery('fire')`, the resulting cache data might look like this: @@ -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 for a first fetch. `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 @@ -114,10 +114,14 @@ const pokemonApi = createApi({ // 3 TS generics: page contents, query arg, page param getInfinitePokemonWithMax: build.infiniteQuery({ infiniteQueryOptions: { + // Must provide a default initial page param value initialPageParam: 1, + // Optionally limit the number of cached pages maxPages: 3, + // Must provide a `getNextPageParam` function getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => lastPageParam + 1, + // Optionally provide a `getPreviousPageParam` function getPreviousPageParam: ( firstPage, allPages, @@ -249,3 +253,255 @@ The endpoint itself only defines `getNextPageParam`, so this example doesn't sup 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]`. + +## Common Infinite Query Patterns + +The `getNext/PreviousPageParam` callbacks offer flexibility in how you interact with the backend API. + +Here are some examples of common infinite query patterns to show how you might approach different use cases. + +### Basic Pagination + +For a simple API that just needs page numbers, you can calculate the previous and next page numbers based on the existing page params: + +```ts no-transpile +const pokemonApi = createApi({ + baseQuery, + endpoints: (build) => ({ + getInfinitePokemon: build.infiniteQuery({ + infiniteQueryOptions: { + initialPageParam: 0, + getNextPageParam: (lastPage, allPages, lastPageParam) => + lastPageParam + 1, + getPreviousPageParam: (firstPage, allPages, firstPageParam) => { + return firstPageParam > 0 ? firstPageParam - 1 : undefined + }, + }, + query({ pageParam }) { + return `https://example.com/listItems?page=${pageParam}` + }, + }), + }), +}) +``` + +### Pagination with Sizes + +For an API that accepts values like page number and page size and includes total pages in the response, you can calculate whether there are more pages remaining: + +```ts no-transpile +type ProjectsResponse = { + projects: Project[] + serverTime: string + totalPages: number +} + +type ProjectsInitialPageParam = { + page: number + size: number +} + +const projectsApi = createApi({ + baseQuery, + endpoints: (build) => ({ + projectsPaginated: build.infiniteQuery< + ProjectsResponse, + void, + ProjectsInitialPageParam + >({ + infiniteQueryOptions: { + initialPageParam: { + page: 0, + size: 20, + }, + getNextPageParam: ( + lastPage, + allPages, + lastPageParam, + allPageParams, + ) => { + const nextPage = lastPageParam.page + 1 + const remainingPages = lastPage?.totalPages - nextPage + + if (remainingPages <= 0) { + return undefined + } + + return { + ...lastPageParam, + page: nextPage, + } + }, + getPreviousPageParam: ( + firstPage, + allPages, + firstPageParam, + allPageParams, + ) => { + const prevPage = firstPageParam.page - 1 + if (prevPage < 0) return undefined + + return { + ...firstPageParam, + page: prevPage, + } + }, + }, + query: ({ pageParam: { page, size } }) => { + return `https://example.com/api/projectsPaginated?page=${page}&size=${size}` + }, + }), + }), +}) +``` + +### Bidirectional Cursors + +If the server sends back cursor values in the response, you can use those as the page params for the next and previous requests: + +```ts no-transpile +type ProjectsCursorPaginated = { + projects: Project[] + serverTime: string + pageInfo: { + startCursor: number + endCursor: number + hasNextPage: boolean + hasPreviousPage: boolean + } +} + +type ProjectsInitialPageParam = { + before?: number + around?: number + after?: number + limit: number +} +type QueryParamLimit = number + +const projectsApi = createApi({ + baseQuery, + endpoints: (build) => ({ + getProjectsBidirectionalCursor: build.infiniteQuery< + ProjectsCursorPaginated, + QueryParamLimit, + ProjectsInitialPageParam + >({ + infiniteQueryOptions: { + initialPageParam: { limit: 10 }, + getPreviousPageParam: ( + firstPage, + allPages, + firstPageParam, + allPageParams, + ) => { + if (!firstPage.pageInfo.hasPreviousPage) { + return undefined + } + return { + before: firstPage.pageInfo.startCursor, + limit: firstPageParam.limit, + } + }, + getNextPageParam: ( + lastPage, + allPages, + lastPageParam, + allPageParams, + ) => { + if (!lastPage.pageInfo.hasNextPage) { + return undefined + } + return { + after: lastPage.pageInfo.endCursor, + limit: lastPageParam.limit, + } + }, + }, + query: ({ pageParam: { before, after, around, limit } }) => { + const params = new URLSearchParams() + params.append('limit', String(limit)) + if (after != null) { + params.append('after', String(after)) + } else if (before != null) { + params.append('before', String(before)) + } else if (around != null) { + params.append('around', String(around)) + } + + return `https://example.com/api/projectsBidirectionalCursor?${params.toString()}`, + }, + }), + }), +}) +``` + +### Limit and Offset + +If the API expects a combination of limit and offset values, those can also be calculated based on the responses and page params. + +```ts no-transpile +export type ProjectsResponse = { + projects: Project[] + numFound: number + serverTime: string +} + +type ProjectsInitialPageParam = { + offset: number + limit: number +} + +const projectsApi = createApi({ + baseQuery, + endpoints: (build) => ({ + projectsLimitOffset: build.infiniteQuery< + ProjectsResponse, + void, + ProjectsInitialPageParam + >({ + infiniteQueryOptions: { + initialPageParam: { + offset: 0, + limit: 20, + }, + getNextPageParam: ( + lastPage, + allPages, + lastPageParam, + allPageParams, + ) => { + const nextOffset = lastPageParam.offset + lastPageParam.limit + const remainingItems = lastPage?.numFound - nextOffset + + if (remainingItems <= 0) { + return undefined + } + + return { + ...lastPageParam, + offset: nextOffset, + } + }, + getPreviousPageParam: ( + firstPage, + allPages, + firstPageParam, + allPageParams, + ) => { + const prevOffset = firstPageParam.offset - firstPageParam.limit + if (prevOffset < 0) return undefined + + return { + ...firstPageParam, + offset: firstPageParam.offset - firstPageParam.limit, + } + }, + }, + query: ({ pageParam: { offset, limit } }) => { + return `https://example.com/api/projectsLimitOffset?offset=${offset}&limit=${limit}` + }, + }), + }), +}) +``` From ac3b76e85b03aec2df36b5848152f22c40b2a61b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 23 Feb 2025 15:20:59 -0500 Subject: [PATCH 4/7] Document infinite query display --- docs/rtk-query/usage/infinite-queries.mdx | 16 +++++++++ .../query/react/infinite-queries/README.md | 33 +++++-------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/docs/rtk-query/usage/infinite-queries.mdx b/docs/rtk-query/usage/infinite-queries.mdx index 0209f2e833..fe7f613ff0 100644 --- a/docs/rtk-query/usage/infinite-queries.mdx +++ b/docs/rtk-query/usage/infinite-queries.mdx @@ -187,6 +187,14 @@ Infinite query hooks return [the same result object as normal query hooks](./que 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. +### Displaying Infinite Query Data + +For infinite query hooks, the `data` field returned by the hook will be the `{pages, pageParams}` structure containing all fetched pages, instead of just a single response value. + +This gives you control over how the data is used for display. You can flatten all the page contents into a single array for infinite scrolling, use the individual page results for pagination, sort or reverse the entries, or any other logic you need for rendering the UI with this data. + +As with any other Redux data, you should avoid mutating these arrays (including calling `array.sort/reverse()` directly on the existing references). + ### Infinite Query Hook Usage Example Here is an example of a typical infinite query endpoint definition, and hook usage in a component: @@ -248,6 +256,12 @@ Similarly, this example relies on manual user clicks on a "Fetch More" button to 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 +## Refetching + +When an infinite query endpoint is refetched (due to tag invalidation, polling, arg change configuration, or manual refetching), RTK Query will try to sequentially refetch all pages currently in the cache. This ensures that the client is always working with the latest data, and avoids stale cursors or duplicate records. + +If the cache entry is ever removed and then re-added, it will start with only fetching the initial page. + ## 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. @@ -260,6 +274,8 @@ The `getNext/PreviousPageParam` callbacks offer flexibility in how you interact Here are some examples of common infinite query patterns to show how you might approach different use cases. +For additional examples, and to see some of these patterns in action, see [the RTK Query "infinite queries" example app in the repo](https://github.com/reduxjs/redux-toolkit/tree/master/examples/query/react/infinite-queries). + ### Basic Pagination For a simple API that just needs page numbers, you can calculate the previous and next page numbers based on the existing page params: diff --git a/examples/query/react/infinite-queries/README.md b/examples/query/react/infinite-queries/README.md index 7247e9edb7..be91f87f0e 100644 --- a/examples/query/react/infinite-queries/README.md +++ b/examples/query/react/infinite-queries/README.md @@ -1,27 +1,10 @@ -# vite-template-redux +# RTK Query Infinite Queries Example -Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library) to create a modern [React](https://react.dev/) app compatible with [Create React App](https://create-react-app.dev/) +This example shows a variety of usage patterns for RTK Query's infinite query endpoint support, including: -```sh -npx degit reduxjs/redux-templates/packages/vite-template-redux my-app -``` - -## Goals - -- Easy migration from Create React App or Vite -- As beginner friendly as Create React App -- Optimized performance compared to Create React App -- Customizable without ejecting - -## Scripts - -- `dev`/`start` - start dev server and open browser -- `build` - build for production -- `preview` - locally preview production build -- `test` - launch test runner - -## Inspiration - -- [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template) -- [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react) -- [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib) +- Basic pagination +- Infinite scrolling +- Bidirectional cursors +- Offset + limit +- Max pages +- React Native FlatList From fed97fa285f6abc3046b3229ce5c8f182100f96c Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 23 Feb 2025 16:22:42 -0500 Subject: [PATCH 5/7] Test and document overlapping fetches --- docs/rtk-query/usage/infinite-queries.mdx | 14 ++- .../src/query/tests/infiniteQueries.test.ts | 95 ++++++++++++++----- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/docs/rtk-query/usage/infinite-queries.mdx b/docs/rtk-query/usage/infinite-queries.mdx index fe7f613ff0..0d0ec7e9c5 100644 --- a/docs/rtk-query/usage/infinite-queries.mdx +++ b/docs/rtk-query/usage/infinite-queries.mdx @@ -256,13 +256,23 @@ Similarly, this example relies on manual user clicks on a "Fetch More" button to 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 -## Refetching +## Infinite Query Behaviors + +### Overlapping Page Fetches + +Since all pages are stored in a single cache entry, there can only be one request in progress at a time. RTK Query already has logic built in to bail out of running a new request if there is already a request in flight for that cache entry. + +That means that if you call `fetchNextPage()` again while an existing request is in progress, the second call won't actually execute a request. Be sure to either await the previous `fetchNextPage()` promise result first or check the `isFetching` flag if you have concerns about a potential request already in progress. + +The promise returned from `fetchNextPage()` does have [a `promise.abort()` method attached](../../api/createAsyncThunk.mdx#canceling-while-running) that will force the earlier request to reject and not save the results. Note that this will mark the cache entry as errored, but the data will still exist. Since `promise.abort()` is synchronous, you would also need to await the previous promise to ensure the rejection is handled, and then trigger the new page fetch. + +### Refetching When an infinite query endpoint is refetched (due to tag invalidation, polling, arg change configuration, or manual refetching), RTK Query will try to sequentially refetch all pages currently in the cache. This ensures that the client is always working with the latest data, and avoids stale cursors or duplicate records. If the cache entry is ever removed and then re-added, it will start with only fetching the initial page. -## Limiting Cache Entry Size +### 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. diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 4d952b5492..6df0e9bb25 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -348,30 +348,6 @@ describe('Infinite queries', () => { }) }) - test.skip('does not break refetching query endpoints', async () => { - const promise0 = storeRef.store.dispatch( - pokemonApi.endpoints.counters.initiate('a'), - ) - - console.log('State after dispatch: ', storeRef.store.getState().api.queries) - - const res0 = await promise0 - - console.log('State after promise: ', storeRef.store.getState().api.queries) - console.log(storeRef.store.getState().actions) - - const promise1 = storeRef.store.dispatch( - pokemonApi.util.upsertQueryData('counters', 'a', { id: 'a', counter: 1 }), - ) - - console.log('State after dispatch: ', storeRef.store.getState().api.queries) - - const res = await promise1 - - console.log('State after promise: ', storeRef.store.getState().api.queries) - console.log(storeRef.store.getState().actions) - }) - test('does not have a page limit without maxPages', async () => { for (let i = 1; i <= 10; i++) { const res = await storeRef.store.dispatch( @@ -643,6 +619,77 @@ describe('Infinite queries', () => { ]) }) + test('Handles multiple next page fetches at once', async () => { + const initialEntry = await storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}), + ) + + checkResultData(initialEntry, [[{ id: '0', name: 'Pokemon 0' }]]) + + expect(queryCounter).toBe(1) + + const promise1 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + direction: 'forward', + }), + ) + + expect(queryCounter).toBe(1) + + const promise2 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + direction: 'forward', + }), + ) + + console.log('Awaiting promises 1 and 2') + + const entry1 = await promise1 + const entry2 = await promise2 + + // The second thunk should have bailed out because the entry was now + // pending, so we should only have sent one request. + expect(queryCounter).toBe(2) + + expect(entry1).toEqual(entry2) + + checkResultData(entry1, [ + [{ id: '0', name: 'Pokemon 0' }], + [{ id: '1', name: 'Pokemon 1' }], + ]) + + expect(queryCounter).toBe(2) + + const promise3 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + direction: 'forward', + }), + ) + + expect(queryCounter).toBe(2) + + // We can abort an existing promise, but due to timing issues, + // we have to await the promise first before triggering the next request. + promise3.abort() + const entry3 = await promise3 + + const promise4 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + direction: 'forward', + }), + ) + + const entry4 = await promise4 + + expect(queryCounter).toBe(4) + + checkResultData(entry4, [ + [{ id: '0', name: 'Pokemon 0' }], + [{ id: '1', name: 'Pokemon 1' }], + [{ id: '2', name: 'Pokemon 2' }], + ]) + }) + test('can fetch pages with refetchOnMountOrArgChange active', async () => { const pokemonApiWithRefetch = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), From 469736b8dcfcc2e0290e61ebe6b0814167552e81 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 23 Feb 2025 17:05:29 -0500 Subject: [PATCH 6/7] Move "React Hooks" page up in the sidebar --- website/sidebars.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/sidebars.ts b/website/sidebars.ts index 9e0deaa1a1..7357c6f873 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -161,9 +161,9 @@ const sidebars: SidebarsConfig = { 'rtk-query/api/created-api/overview', 'rtk-query/api/created-api/redux-integration', 'rtk-query/api/created-api/endpoints', + 'rtk-query/api/created-api/hooks', 'rtk-query/api/created-api/code-splitting', 'rtk-query/api/created-api/api-slice-utils', - 'rtk-query/api/created-api/hooks', ], }, ], From 729a1020b92421baff7138018a516b48618ae1d1 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 23 Feb 2025 17:05:57 -0500 Subject: [PATCH 7/7] Code review cleanup --- docs/rtk-query/api/createApi.mdx | 21 ++++---- docs/rtk-query/api/created-api/hooks.mdx | 16 +++--- packages/toolkit/src/query/core/apiState.ts | 14 ++--- .../toolkit/src/query/core/buildInitiate.ts | 2 - .../core/buildMiddleware/cacheLifecycle.ts | 1 - .../core/buildMiddleware/queryLifecycle.ts | 1 - .../toolkit/src/query/endpointDefinitions.ts | 53 +++++++++---------- .../toolkit/src/query/react/buildHooks.ts | 3 -- .../src/query/tests/infiniteQueries.test.ts | 2 - 9 files changed, 50 insertions(+), 63 deletions(-) diff --git a/docs/rtk-query/api/createApi.mdx b/docs/rtk-query/api/createApi.mdx index b88bfc933f..d881735cfb 100644 --- a/docs/rtk-query/api/createApi.mdx +++ b/docs/rtk-query/api/createApi.mdx @@ -273,28 +273,28 @@ export type InfiniteQueryDefinition< */ initialPageParam: PageParam /** - * If specified, only keep this many pages in cache at once. - * If additional pages are fetched, older pages in the other - * direction will be dropped from the cache. + * This function is required to automatically get the next cursor for infinite queries. + * The result will also be used to determine the value of `hasNextPage`. */ - maxPages?: number + getNextPageParam: PageParamFunction /** * This function can be set to automatically get the previous cursor for infinite queries. * The result will also be used to determine the value of `hasPreviousPage`. */ getPreviousPageParam?: PageParamFunction /** - * This function is required to automatically get the next cursor for infinite queries. - * The result will also be used to determine the value of `hasNextPage`. + * If specified, only keep this many pages in cache at once. + * If additional pages are fetched, older pages in the other + * direction will be dropped from the cache. */ - getNextPageParam: PageParamFunction + maxPages?: number } } ``` #### Mutation endpoint definition -Mutation endpoints (defined with build.mutation()`) are used to send updates to the server, and force invalidation and refetching of query endpoints. +Mutation endpoints (defined with `build.mutation()`) are used to send updates to the server, and force invalidation and refetching of query endpoints. As with queries, you must specify either the `query` option or the `queryFn` async method. @@ -543,12 +543,9 @@ _(only for `infiniteQuery` endpoints)_ The `infiniteQueryOptions` field includes: - `initialPageParam`: the default page param value used for the first request, if this was not specified at the usage site -- `maxPages`: an optional limit to how many fetched pages will be kept in the cache entry at a time - `getNextPageParam`: a required callback you must provide to calculate the next page param, given the existing cached pages and page params - `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. +- `maxPages`: an optional limit to how many fetched pages will be kept in the cache entry at a time [examples](docblock://query/endpointDefinitions.ts?token=InfiniteQueryExtraOptions.infiniteQueryOptions) diff --git a/docs/rtk-query/api/created-api/hooks.mdx b/docs/rtk-query/api/created-api/hooks.mdx index d0a59d490b..333f91ee7e 100644 --- a/docs/rtk-query/api/created-api/hooks.mdx +++ b/docs/rtk-query/api/created-api/hooks.mdx @@ -11,15 +11,15 @@ hide_title: true ## Hooks Overview -The core RTK Query `createApi` method is UI-agnostic, in the same way that the Redux core library and Redux Toolkit are UI-agnostic. They are all plain JS logic that can be used anywhere. +The core RTK Query `createApi` method is UI-agnostic, in the same way that the Redux core library and Redux Toolkit are UI-agnostic. They are all plain JS logic that can be used anywhere. So, if you import `createApi` from `'@reduxjs/toolkit/query'`, it does not have any specific UI integrations included. -However, RTK Query also provides the ability to auto-generate React hooks for each of your endpoints. Since this specifically depends on React itself, RTK Query provides an alternate entry point that exposes a customized version of `createApi` that includes that functionality: +However, RTK Query also provides the ability to auto-generate React hooks for each of your endpoints. Since this specifically depends on React itself, RTK Query provides an additional entry point that exposes a customized version of `createApi` that includes that functionality: ```ts no-transpile import { createApi } from '@reduxjs/toolkit/query/react' ``` -If you have used the React-specific version of `createApi`, the generated `api` slice structure will also contain a set of React hooks. The primary endpoint hooks are available as `api.endpoints[endpointName].useQuery` or `api.endpoints[endpointName].useMutation`, matching how you defined that endpoint. +If you have used the React-specific version of `createApi`, the generated `api` slice structure will also contain a set of React hooks. The primary endpoint hooks are available as `api.endpoints[endpointName].useQuery`, `api.endpoints[endpointName].useMutation`, and `api.endpoints[endpointName].useInfiniteQuery`, matching how you defined that endpoint. ### Generated Hook Names @@ -37,7 +37,7 @@ const { data } = api.useGetPostsQuery() const [updatePost, { data }] = api.useUpdatePostMutation() ``` -The general format is `use(Endpointname)(Query|Mutation)` - `use` is prefixed, the first letter of your endpoint name is capitalized, then `Query` or `Mutation` is appended depending on the type. +The general format is `use(Endpointname)(Query|Mutation|InfiniteQuery)` - `use` is prefixed, the first letter of your endpoint name is capitalized, then `Query` or `Mutation` or `InfiniteQuery` is appended depending on the type. ### Available Hooks @@ -45,7 +45,7 @@ RTK Query provides additional hooks for more advanced use-cases, although not al Most of the hooks are generated on a per-endpoint basis. -The full list of hooks generated in the React-specific version of `createApi` is as follows: +The full list of hooks generated in the React-specific version of `createApi` is: - Endpoint-specific, generated the `api` object with a unique name and on the endpoint object with a generic name: - [`useQuery`](#usequery) (all standard queries) @@ -54,10 +54,10 @@ The full list of hooks generated in the React-specific version of `createApi` is - [`useLazyQuery`](#uselazyquery) (all standard queries) - Endpoint-specific, only generated on the endpoint object with a generic name: - [`useQueryState`](#usequerystate) - - [`useQuerySubscription](#usequerysubscription) - - [`useLazyQuerySubscription](#uselazyquerysubscription) + - [`useQuerySubscription`](#usequerysubscription) + - [`useLazyQuerySubscription`](#uselazyquerysubscription) - [`useInfiniteQueryState`](#useinfinitequerystate) - - [`useInfiniteQuerySubscription](#useinfinitequerysubscription) + - [`useInfiniteQuerySubscription`](#useinfinitequerysubscription) - Endpoint-agnostic, generated on the `api` object: - [`usePrefetch`](#useprefetch) diff --git a/packages/toolkit/src/query/core/apiState.ts b/packages/toolkit/src/query/core/apiState.ts index 53f1becba5..9a3f6800f0 100644 --- a/packages/toolkit/src/query/core/apiState.ts +++ b/packages/toolkit/src/query/core/apiState.ts @@ -43,21 +43,21 @@ export type InfiniteQueryConfigOptions = { */ initialPageParam: PageParam /** - * If specified, only keep this many pages in cache at once. - * If additional pages are fetched, older pages in the other - * direction will be dropped from the cache. + * This function is required to automatically get the next cursor for infinite queries. + * The result will also be used to determine the value of `hasNextPage`. */ - maxPages?: number + getNextPageParam: PageParamFunction /** * This function can be set to automatically get the previous cursor for infinite queries. * The result will also be used to determine the value of `hasPreviousPage`. */ getPreviousPageParam?: PageParamFunction /** - * This function is required to automatically get the next cursor for infinite queries. - * The result will also be used to determine the value of `hasNextPage`. + * If specified, only keep this many pages in cache at once. + * If additional pages are fetched, older pages in the other + * direction will be dropped from the cache. */ - getNextPageParam: PageParamFunction + maxPages?: number } export interface InfiniteData { diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index 72171fdae2..9d5f453a4b 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -96,8 +96,6 @@ type StartQueryActionCreator< options?: StartQueryActionCreatorOptions, ) => ThunkAction, any, any, UnknownAction> -// placeholder type which -// may attempt to derive the list of args to query in pagination export type StartInfiniteQueryActionCreator< D extends InfiniteQueryDefinition, > = ( diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 6783ab8a4e..1ec2a86327 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -142,7 +142,6 @@ export type CacheLifecycleQueryExtraOptions< ): Promise | void } -// copying QueryDefinition to get past initial build export type CacheLifecycleInfiniteQueryExtraOptions< ResultType, QueryArg, diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts index d23370bc11..27aa0397ea 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts @@ -121,7 +121,6 @@ export type QueryLifecycleQueryExtraOptions< ): Promise | void } -// temporarily cloned QueryOptions again to just get the definition to build for now export type QueryLifecycleInfiniteQueryExtraOptions< ResultType, QueryArg, diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 888700819d..44e46e3519 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -219,7 +219,6 @@ export type BaseEndpointDefinition< export enum DefinitionType { query = 'query', mutation = 'mutation', - // hijacking query temporarily to get the definition to build infinitequery = 'infinitequery', } @@ -545,7 +544,6 @@ export type QueryDefinition< > = BaseEndpointDefinition & QueryExtraOptions -// cloning Query Endpoint Definition with an extra option to begin with export interface InfiniteQueryTypes< QueryArg, PageParam, @@ -613,6 +611,7 @@ export interface InfiniteQueryExtraOptions< * ensure the infinite query can properly fetch the next page of data. * `initialPageParam` may be specified when using the * endpoint, to override the default value. + * `maxPages` and `getPreviousPageParam` are both optional. * * @example * @@ -625,30 +624,30 @@ export interface InfiniteQueryExtraOptions< * name: string * } * - const pokemonApi = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), - endpoints: (build) => ({ - getInfinitePokemonWithMax: build.infiniteQuery({ - infiniteQueryOptions: { - initialPageParam: 0, - maxPages: 3, - getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => - lastPageParam + 1, - getPreviousPageParam: ( - firstPage, - allPages, - firstPageParam, - allPageParams, - ) => { - return firstPageParam > 0 ? firstPageParam - 1 : undefined - }, - }, - query({pageParam}) { - return `https://example.com/listItems?page=${pageParam}` - }, - }), - }), - }) + * const pokemonApi = createApi({ + * baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }), + * endpoints: (build) => ({ + * getInfinitePokemonWithMax: build.infiniteQuery({ + * infiniteQueryOptions: { + * initialPageParam: 0, + * maxPages: 3, + * getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + * lastPageParam + 1, + * getPreviousPageParam: ( + * firstPage, + * allPages, + * firstPageParam, + * allPageParams, + * ) => { + * return firstPageParam > 0 ? firstPageParam - 1 : undefined + * }, + * }, + * query({pageParam}) { + * return `https://example.com/listItems?page=${pageParam}` + * }, + * }), + * }), + * }) * ``` */ @@ -736,7 +735,7 @@ export type InfiniteQueryDefinition< ResultType, ReducerPath extends string = string, > = - // Intentionally use `PageParam` as the `QueryArg` type + // Infinite query endpoints receive `{queryArg, pageParam}` BaseEndpointDefinition< InfiniteQueryCombinedArg, BaseQuery, diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 184df19009..1f08eab47b 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -1913,13 +1913,10 @@ export function buildHooks({ return useMemo(() => { const fetchNextPage = () => { - // TODO the hasNextPage bailout breaks things - //if (!hasNextPage) return return trigger(arg, 'forward') } const fetchPreviousPage = () => { - //if (!hasPreviousPage) return return trigger(arg, 'backward') } diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 6df0e9bb25..dd4ca4e4ac 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -642,8 +642,6 @@ describe('Infinite queries', () => { }), ) - console.log('Awaiting promises 1 and 2') - const entry1 = await promise1 const entry2 = await promise2