Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/toolkit/src/query/core/apiState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type GetPreviousPageParamFunction<TPageParam, TQueryFnData> = (

export type InfiniteQueryConfigOptions<TQueryFnData, TPageParam> = {
initialPageParam: TPageParam
maxPages?: number
/**
* 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`.
Expand Down
21 changes: 11 additions & 10 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export function buildThunks<
const fetchPage = async (
data: InfiniteData<unknown, unknown>,
param: unknown,
maxPages: number,
previous?: boolean,
): Promise<QueryReturnValue> => {
// This should handle cases where there is no `getPrevPageParam`,
Expand All @@ -442,7 +443,6 @@ export function buildThunks<
const pageResponse = await executeRequest(param)

// TODO Get maxPages from endpoint config
const maxPages = 20
const addTo = previous ? addToStart : addToEnd

return {
Expand Down Expand Up @@ -535,6 +535,10 @@ export function buildThunks<
'infiniteQueryOptions' in endpointDefinition
) {
// This is an infinite query endpoint
const { infiniteQueryOptions } = endpointDefinition

// Runtime checks should guarantee this is a positive number if provided
const { maxPages = Infinity } = infiniteQueryOptions

let result: QueryReturnValue

Expand All @@ -551,24 +555,20 @@ export function buildThunks<
if ('direction' in arg && arg.direction && existingData.pages.length) {
const previous = arg.direction === 'backward'
const pageParamFn = previous ? getPreviousPageParam : getNextPageParam
const param = pageParamFn(
endpointDefinition.infiniteQueryOptions,
existingData,
)
const param = pageParamFn(infiniteQueryOptions, existingData)

result = await fetchPage(existingData, param, previous)
result = await fetchPage(existingData, param, maxPages, previous)
} else {
// Otherwise, fetch the first page and then any remaining pages

const {
initialPageParam = endpointDefinition.infiniteQueryOptions
.initialPageParam,
} = arg as InfiniteQueryThunkArg<any>
const { initialPageParam = infiniteQueryOptions.initialPageParam } =
arg as InfiniteQueryThunkArg<any>

// Fetch first page
result = await fetchPage(
existingData,
existingData.pageParams[0] ?? initialPageParam,
maxPages,
)

//original
Expand All @@ -587,6 +587,7 @@ export function buildThunks<
result = await fetchPage(
result.data as InfiniteData<unknown, unknown>,
param,
maxPages,
)
}
}
Expand Down
33 changes: 31 additions & 2 deletions packages/toolkit/src/query/createApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
EndpointBuilder,
EndpointDefinitions,
} from './endpointDefinitions'
import { DefinitionType, isQueryDefinition } from './endpointDefinitions'
import {
DefinitionType,
isInfiniteQueryDefinition,
isQueryDefinition,
} from './endpointDefinitions'
import { nanoid } from './core/rtkImports'
import type { UnknownAction } from '@reduxjs/toolkit'
import type { NoInfer } from './tsHelpers'
Expand Down Expand Up @@ -347,7 +351,8 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
const evaluatedEndpoints = inject.endpoints({
query: (x) => ({ ...x, type: DefinitionType.query }) as any,
mutation: (x) => ({ ...x, type: DefinitionType.mutation }) as any,
infiniteQuery: (x) => ({ ...x, type: DefinitionType.infinitequery } as any),
infiniteQuery: (x) =>
({ ...x, type: DefinitionType.infinitequery }) as any,
})

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

if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
if (isInfiniteQueryDefinition(definition)) {
const { infiniteQueryOptions } = definition
const { maxPages, getPreviousPageParam } = infiniteQueryOptions

if (typeof maxPages === 'number') {
if (maxPages < 1) {
throw new Error(
`maxPages for endpoint '${endpointName}' must be a number greater than 0`,
)
}

if (typeof getPreviousPageParam !== 'function') {
throw new Error(
`getPreviousPageParam for endpoint '${endpointName}' must be a function if maxPages is used`,
)
}
}
}
}

context.endpointDefinitions[endpointName] = definition
for (const m of initializedModules) {
m.injectEndpoint(endpointName, definition)
Expand Down
147 changes: 130 additions & 17 deletions packages/toolkit/src/query/tests/infiniteQueries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
QueryStatus,
createApi,
fetchBaseQuery,
fakeBaseQuery,
skipToken,
} from '@reduxjs/toolkit/query/react'
import {
Expand Down Expand Up @@ -46,8 +47,6 @@ describe('Infinite queries', () => {
const pokemonApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
// GOAL: Specify both the query arg (for cache key serialization)
// and the page param type (for feeding into the query URL)
getInfinitePokemon: builder.infiniteQuery<Pokemon[], string, number>({
infiniteQueryOptions: {
initialPageParam: 0,
Expand All @@ -67,23 +66,67 @@ describe('Infinite queries', () => {
return firstPageParam > 0 ? firstPageParam - 1 : undefined
},
},

// Actual query arg type should be `number`
query(pageParam) {
return `https://example.com/listItems?page=${pageParam}`
},
}),
getInfinitePokemonWithMax: builder.infiniteQuery<
Pokemon[],
string,
number
>({
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}`
},
}),
counter: builder.query<number, string>({
queryFn: async () => {
return { data: 0 }
},
}),
}),
})

let storeRef = setupApiStore(pokemonApi, undefined, {
withoutTestLifecycles: true,
})
let storeRef = setupApiStore(
pokemonApi,
{ ...actionsReducer },
{
withoutTestLifecycles: true,
},
)

beforeEach(() => {
storeRef = setupApiStore(pokemonApi, undefined, {
withoutTestLifecycles: true,
})
storeRef = setupApiStore(
pokemonApi,
{ ...actionsReducer },
{
withoutTestLifecycles: true,
},
)

process.env.NODE_ENV = 'development'
})

afterEach(() => {
process.env.NODE_ENV = 'test'
})

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

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

if (entry1PrevPageMissing.status === QueryStatus.fulfilled) {
// There is no p
expect(entry1PrevPageMissing.data.pages).toEqual([
// two pages, one entry each
[{ id: '0', name: 'Pokemon 0' }],
[{ id: '1', name: 'Pokemon 1' }],
])
}

// console.log(
// 'API state: ',
// util.inspect(storeRef.store.getState().api, { depth: Infinity }),
// )

const entry2InitialLoad = await storeRef.store.dispatch(
pokemonApi.endpoints.getInfinitePokemon.initiate('water', {
initialPageParam: 3,
Expand Down Expand Up @@ -181,4 +217,81 @@ describe('Infinite queries', () => {
])
}
})

test('does not have a page limit without maxPages', async () => {
for (let i = 1; i <= 10; i++) {
const res = await storeRef.store.dispatch(
pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {
direction: 'forward',
}),
)

if (res.status === QueryStatus.fulfilled) {
expect(res.data.pages).toHaveLength(i)
}
}
})

test('applies a page limit with maxPages', async () => {
for (let i = 1; i <= 10; i++) {
const res = await storeRef.store.dispatch(
pokemonApi.endpoints.getInfinitePokemonWithMax.initiate('fire', {
direction: 'forward',
}),
)
if (res.status === QueryStatus.fulfilled) {
// Should have 1, 2, 3 (repeating) pages
expect(res.data.pages).toHaveLength(Math.min(i, 3))
}
}

// Should now have entries 7, 8, 9 after the loop

const res = await storeRef.store.dispatch(
pokemonApi.endpoints.getInfinitePokemonWithMax.initiate('fire', {
direction: 'backward',
}),
)

if (res.status === QueryStatus.fulfilled) {
// When we go back 1, we now have 6, 7, 8
expect(res.data.pages).toEqual([
[{ id: '6', name: 'Pokemon 6' }],
[{ id: '7', name: 'Pokemon 7' }],
[{ id: '8', name: 'Pokemon 8' }],
])
}
})

test('validates maxPages during createApi call', async () => {
const createApiWithMaxPages = (
maxPages: number,
getPreviousPageParam: (() => number) | undefined,
) => {
createApi({
baseQuery: fakeBaseQuery(),
endpoints: (build) => ({
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
query(pageParam) {
return `https://example.com/listItems?page=${pageParam}`
},
infiniteQueryOptions: {
initialPageParam: 0,
maxPages,
getNextPageParam: () => 1,
getPreviousPageParam,
},
}),
}),
})
}

expect(() => createApiWithMaxPages(0, () => 0)).toThrowError(
`maxPages for endpoint 'getInfinitePokemon' must be a number greater than 0`,
)

expect(() => createApiWithMaxPages(1, undefined)).toThrowError(
`getPreviousPageParam for endpoint 'getInfinitePokemon' must be a function if maxPages is used`,
)
})
})