Skip to content

Commit 435d82a

Browse files
authored
Merge pull request #4739 from reduxjs/feature/infinite-query-improvements-maxPages
Add maxPages support
2 parents 1e5789f + fc3341a commit 435d82a

File tree

4 files changed

+173
-29
lines changed

4 files changed

+173
-29
lines changed

packages/toolkit/src/query/core/apiState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type GetPreviousPageParamFunction<TPageParam, TQueryFnData> = (
4646

4747
export type InfiniteQueryConfigOptions<TQueryFnData, TPageParam> = {
4848
initialPageParam: TPageParam
49+
maxPages?: number
4950
/**
5051
* This function can be set to automatically get the previous cursor for infinite queries.
5152
* The result will also be used to determine the value of `hasPreviousPage`.

packages/toolkit/src/query/core/buildThunks.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ export function buildThunks<
431431
const fetchPage = async (
432432
data: InfiniteData<unknown, unknown>,
433433
param: unknown,
434+
maxPages: number,
434435
previous?: boolean,
435436
): Promise<QueryReturnValue> => {
436437
// This should handle cases where there is no `getPrevPageParam`,
@@ -442,7 +443,6 @@ export function buildThunks<
442443
const pageResponse = await executeRequest(param)
443444

444445
// TODO Get maxPages from endpoint config
445-
const maxPages = 20
446446
const addTo = previous ? addToStart : addToEnd
447447

448448
return {
@@ -535,6 +535,10 @@ export function buildThunks<
535535
'infiniteQueryOptions' in endpointDefinition
536536
) {
537537
// This is an infinite query endpoint
538+
const { infiniteQueryOptions } = endpointDefinition
539+
540+
// Runtime checks should guarantee this is a positive number if provided
541+
const { maxPages = Infinity } = infiniteQueryOptions
538542

539543
let result: QueryReturnValue
540544

@@ -551,24 +555,20 @@ export function buildThunks<
551555
if ('direction' in arg && arg.direction && existingData.pages.length) {
552556
const previous = arg.direction === 'backward'
553557
const pageParamFn = previous ? getPreviousPageParam : getNextPageParam
554-
const param = pageParamFn(
555-
endpointDefinition.infiniteQueryOptions,
556-
existingData,
557-
)
558+
const param = pageParamFn(infiniteQueryOptions, existingData)
558559

559-
result = await fetchPage(existingData, param, previous)
560+
result = await fetchPage(existingData, param, maxPages, previous)
560561
} else {
561562
// Otherwise, fetch the first page and then any remaining pages
562563

563-
const {
564-
initialPageParam = endpointDefinition.infiniteQueryOptions
565-
.initialPageParam,
566-
} = arg as InfiniteQueryThunkArg<any>
564+
const { initialPageParam = infiniteQueryOptions.initialPageParam } =
565+
arg as InfiniteQueryThunkArg<any>
567566

568567
// Fetch first page
569568
result = await fetchPage(
570569
existingData,
571570
existingData.pageParams[0] ?? initialPageParam,
571+
maxPages,
572572
)
573573

574574
//original
@@ -587,6 +587,7 @@ export function buildThunks<
587587
result = await fetchPage(
588588
result.data as InfiniteData<unknown, unknown>,
589589
param,
590+
maxPages,
590591
)
591592
}
592593
}

packages/toolkit/src/query/createApi.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type {
77
EndpointBuilder,
88
EndpointDefinitions,
99
} from './endpointDefinitions'
10-
import { DefinitionType, isQueryDefinition } from './endpointDefinitions'
10+
import {
11+
DefinitionType,
12+
isInfiniteQueryDefinition,
13+
isQueryDefinition,
14+
} from './endpointDefinitions'
1115
import { nanoid } from './core/rtkImports'
1216
import type { UnknownAction } from '@reduxjs/toolkit'
1317
import type { NoInfer } from './tsHelpers'
@@ -347,7 +351,8 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
347351
const evaluatedEndpoints = inject.endpoints({
348352
query: (x) => ({ ...x, type: DefinitionType.query }) as any,
349353
mutation: (x) => ({ ...x, type: DefinitionType.mutation }) as any,
350-
infiniteQuery: (x) => ({ ...x, type: DefinitionType.infinitequery } as any),
354+
infiniteQuery: (x) =>
355+
({ ...x, type: DefinitionType.infinitequery }) as any,
351356
})
352357

353358
for (const [endpointName, definition] of Object.entries(
@@ -373,6 +378,30 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
373378
continue
374379
}
375380

381+
if (
382+
typeof process !== 'undefined' &&
383+
process.env.NODE_ENV === 'development'
384+
) {
385+
if (isInfiniteQueryDefinition(definition)) {
386+
const { infiniteQueryOptions } = definition
387+
const { maxPages, getPreviousPageParam } = infiniteQueryOptions
388+
389+
if (typeof maxPages === 'number') {
390+
if (maxPages < 1) {
391+
throw new Error(
392+
`maxPages for endpoint '${endpointName}' must be a number greater than 0`,
393+
)
394+
}
395+
396+
if (typeof getPreviousPageParam !== 'function') {
397+
throw new Error(
398+
`getPreviousPageParam for endpoint '${endpointName}' must be a function if maxPages is used`,
399+
)
400+
}
401+
}
402+
}
403+
}
404+
376405
context.endpointDefinitions[endpointName] = definition
377406
for (const m of initializedModules) {
378407
m.injectEndpoint(endpointName, definition)

packages/toolkit/src/query/tests/infiniteQueries.test.ts

Lines changed: 130 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
QueryStatus,
1515
createApi,
1616
fetchBaseQuery,
17+
fakeBaseQuery,
1718
skipToken,
1819
} from '@reduxjs/toolkit/query/react'
1920
import {
@@ -46,8 +47,6 @@ describe('Infinite queries', () => {
4647
const pokemonApi = createApi({
4748
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
4849
endpoints: (builder) => ({
49-
// GOAL: Specify both the query arg (for cache key serialization)
50-
// and the page param type (for feeding into the query URL)
5150
getInfinitePokemon: builder.infiniteQuery<Pokemon[], string, number>({
5251
infiniteQueryOptions: {
5352
initialPageParam: 0,
@@ -67,23 +66,67 @@ describe('Infinite queries', () => {
6766
return firstPageParam > 0 ? firstPageParam - 1 : undefined
6867
},
6968
},
70-
71-
// Actual query arg type should be `number`
7269
query(pageParam) {
7370
return `https://example.com/listItems?page=${pageParam}`
7471
},
7572
}),
73+
getInfinitePokemonWithMax: builder.infiniteQuery<
74+
Pokemon[],
75+
string,
76+
number
77+
>({
78+
infiniteQueryOptions: {
79+
initialPageParam: 0,
80+
maxPages: 3,
81+
getNextPageParam: (
82+
lastPage,
83+
allPages,
84+
lastPageParam,
85+
allPageParams,
86+
) => lastPageParam + 1,
87+
getPreviousPageParam: (
88+
firstPage,
89+
allPages,
90+
firstPageParam,
91+
allPageParams,
92+
) => {
93+
return firstPageParam > 0 ? firstPageParam - 1 : undefined
94+
},
95+
},
96+
query(pageParam) {
97+
return `https://example.com/listItems?page=${pageParam}`
98+
},
99+
}),
100+
counter: builder.query<number, string>({
101+
queryFn: async () => {
102+
return { data: 0 }
103+
},
104+
}),
76105
}),
77106
})
78107

79-
let storeRef = setupApiStore(pokemonApi, undefined, {
80-
withoutTestLifecycles: true,
81-
})
108+
let storeRef = setupApiStore(
109+
pokemonApi,
110+
{ ...actionsReducer },
111+
{
112+
withoutTestLifecycles: true,
113+
},
114+
)
82115

83116
beforeEach(() => {
84-
storeRef = setupApiStore(pokemonApi, undefined, {
85-
withoutTestLifecycles: true,
86-
})
117+
storeRef = setupApiStore(
118+
pokemonApi,
119+
{ ...actionsReducer },
120+
{
121+
withoutTestLifecycles: true,
122+
},
123+
)
124+
125+
process.env.NODE_ENV = 'development'
126+
})
127+
128+
afterEach(() => {
129+
process.env.NODE_ENV = 'test'
87130
})
88131

89132
test('Basic infinite query behavior', async () => {
@@ -110,7 +153,6 @@ describe('Infinite queries', () => {
110153
)
111154

112155
expect(entry1SecondPage.status).toBe(QueryStatus.fulfilled)
113-
// console.log('Value: ', util.inspect(entry1SecondPage, { depth: Infinity }))
114156
if (entry1SecondPage.status === QueryStatus.fulfilled) {
115157
expect(entry1SecondPage.data.pages).toEqual([
116158
// two pages, one entry each
@@ -126,19 +168,13 @@ describe('Infinite queries', () => {
126168
)
127169

128170
if (entry1PrevPageMissing.status === QueryStatus.fulfilled) {
129-
// There is no p
130171
expect(entry1PrevPageMissing.data.pages).toEqual([
131172
// two pages, one entry each
132173
[{ id: '0', name: 'Pokemon 0' }],
133174
[{ id: '1', name: 'Pokemon 1' }],
134175
])
135176
}
136177

137-
// console.log(
138-
// 'API state: ',
139-
// util.inspect(storeRef.store.getState().api, { depth: Infinity }),
140-
// )
141-
142178
const entry2InitialLoad = await storeRef.store.dispatch(
143179
pokemonApi.endpoints.getInfinitePokemon.initiate('water', {
144180
initialPageParam: 3,
@@ -181,4 +217,81 @@ describe('Infinite queries', () => {
181217
])
182218
}
183219
})
220+
221+
test('does not have a page limit without maxPages', async () => {
222+
for (let i = 1; i <= 10; i++) {
223+
const res = await storeRef.store.dispatch(
224+
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
225+
direction: 'forward',
226+
}),
227+
)
228+
229+
if (res.status === QueryStatus.fulfilled) {
230+
expect(res.data.pages).toHaveLength(i)
231+
}
232+
}
233+
})
234+
235+
test('applies a page limit with maxPages', async () => {
236+
for (let i = 1; i <= 10; i++) {
237+
const res = await storeRef.store.dispatch(
238+
pokemonApi.endpoints.getInfinitePokemonWithMax.initiate('fire', {
239+
direction: 'forward',
240+
}),
241+
)
242+
if (res.status === QueryStatus.fulfilled) {
243+
// Should have 1, 2, 3 (repeating) pages
244+
expect(res.data.pages).toHaveLength(Math.min(i, 3))
245+
}
246+
}
247+
248+
// Should now have entries 7, 8, 9 after the loop
249+
250+
const res = await storeRef.store.dispatch(
251+
pokemonApi.endpoints.getInfinitePokemonWithMax.initiate('fire', {
252+
direction: 'backward',
253+
}),
254+
)
255+
256+
if (res.status === QueryStatus.fulfilled) {
257+
// When we go back 1, we now have 6, 7, 8
258+
expect(res.data.pages).toEqual([
259+
[{ id: '6', name: 'Pokemon 6' }],
260+
[{ id: '7', name: 'Pokemon 7' }],
261+
[{ id: '8', name: 'Pokemon 8' }],
262+
])
263+
}
264+
})
265+
266+
test('validates maxPages during createApi call', async () => {
267+
const createApiWithMaxPages = (
268+
maxPages: number,
269+
getPreviousPageParam: (() => number) | undefined,
270+
) => {
271+
createApi({
272+
baseQuery: fakeBaseQuery(),
273+
endpoints: (build) => ({
274+
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
275+
query(pageParam) {
276+
return `https://example.com/listItems?page=${pageParam}`
277+
},
278+
infiniteQueryOptions: {
279+
initialPageParam: 0,
280+
maxPages,
281+
getNextPageParam: () => 1,
282+
getPreviousPageParam,
283+
},
284+
}),
285+
}),
286+
})
287+
}
288+
289+
expect(() => createApiWithMaxPages(0, () => 0)).toThrowError(
290+
`maxPages for endpoint 'getInfinitePokemon' must be a number greater than 0`,
291+
)
292+
293+
expect(() => createApiWithMaxPages(1, undefined)).toThrowError(
294+
`getPreviousPageParam for endpoint 'getInfinitePokemon' must be a function if maxPages is used`,
295+
)
296+
})
184297
})

0 commit comments

Comments
 (0)