@@ -34,8 +34,8 @@ import {
3434import { userEvent } from '@testing-library/user-event'
3535import type { SyncScreen } from '@testing-library/react-render-stream/pure'
3636import { createRenderStream } from '@testing-library/react-render-stream/pure'
37- import { HttpResponse , http } from 'msw'
38- import { useEffect , useState } from 'react'
37+ import { HttpResponse , http , delay } from 'msw'
38+ import { useEffect , useMemo , useState } from 'react'
3939import type { InfiniteQueryResultFlags } from '../core/buildSelectors'
4040
4141// Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
@@ -929,9 +929,6 @@ describe('hooks tests', () => {
929929 // See https://github.com/reduxjs/redux-toolkit/issues/4267 - Memory leak in useQuery rapid query arg changes
930930 test ( 'Hook subscriptions are properly cleaned up when query is fulfilled/rejected' , async ( ) => {
931931 // This is imported already, but it seems to be causing issues with the test on certain matrixes
932- function delay ( ms : number ) {
933- return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
934- }
935932
936933 const pokemonApi = createApi ( {
937934 baseQuery : fetchBaseQuery ( { baseUrl : 'https://pokeapi.co/api/v2/' } ) ,
@@ -1974,6 +1971,250 @@ describe('hooks tests', () => {
19741971 hasPreviousPage : true ,
19751972 } )
19761973 } )
1974+
1975+ test ( 'Object page params does not keep forcing refetching' , async ( ) => {
1976+ type Project = {
1977+ id : number
1978+ createdAt : string
1979+ }
1980+
1981+ type ProjectsResponse = {
1982+ projects : Project [ ]
1983+ numFound : number
1984+ serverTime : string
1985+ }
1986+
1987+ interface ProjectsInitialPageParam {
1988+ offset : number
1989+ limit : number
1990+ }
1991+
1992+ const apiWithInfiniteScroll = createApi ( {
1993+ baseQuery : fetchBaseQuery ( { baseUrl : 'https://example.com/' } ) ,
1994+ endpoints : ( builder ) => ( {
1995+ projectsLimitOffset : builder . infiniteQuery <
1996+ ProjectsResponse ,
1997+ void ,
1998+ ProjectsInitialPageParam
1999+ > ( {
2000+ infiniteQueryOptions : {
2001+ initialPageParam : {
2002+ offset : 0 ,
2003+ limit : 20 ,
2004+ } ,
2005+ getNextPageParam : (
2006+ lastPage ,
2007+ allPages ,
2008+ lastPageParam ,
2009+ allPageParams ,
2010+ ) => {
2011+ const nextOffset = lastPageParam . offset + lastPageParam . limit
2012+ const remainingItems = lastPage ?. numFound - nextOffset
2013+
2014+ if ( remainingItems <= 0 ) {
2015+ return undefined
2016+ }
2017+
2018+ return {
2019+ ...lastPageParam ,
2020+ offset : nextOffset ,
2021+ }
2022+ } ,
2023+ getPreviousPageParam : (
2024+ firstPage ,
2025+ allPages ,
2026+ firstPageParam ,
2027+ allPageParams ,
2028+ ) => {
2029+ const prevOffset = firstPageParam . offset - firstPageParam . limit
2030+ if ( prevOffset < 0 ) return undefined
2031+
2032+ return {
2033+ ...firstPageParam ,
2034+ offset : firstPageParam . offset - firstPageParam . limit ,
2035+ }
2036+ } ,
2037+ } ,
2038+ query : ( { offset, limit } ) => {
2039+ return {
2040+ url : `https://example.com/api/projectsLimitOffset?offset=${ offset } &limit=${ limit } ` ,
2041+ method : 'GET' ,
2042+ }
2043+ } ,
2044+ } ) ,
2045+ } ) ,
2046+ } )
2047+
2048+ const projects = Array . from ( { length : 50 } , ( _ , i ) => {
2049+ return {
2050+ id : i ,
2051+ createdAt : Date . now ( ) + i * 1000 ,
2052+ }
2053+ } )
2054+
2055+ let numRequests = 0
2056+
2057+ server . use (
2058+ http . get (
2059+ 'https://example.com/api/projectsLimitOffset' ,
2060+ async ( { request } ) => {
2061+ const url = new URL ( request . url )
2062+ const limit = parseInt ( url . searchParams . get ( 'limit' ) ?? '5' , 10 )
2063+ let offset = parseInt ( url . searchParams . get ( 'offset' ) ?? '0' , 10 )
2064+
2065+ numRequests ++
2066+
2067+ if ( isNaN ( offset ) || offset < 0 ) {
2068+ offset = 0
2069+ }
2070+ if ( isNaN ( limit ) || limit <= 0 ) {
2071+ return HttpResponse . json (
2072+ {
2073+ message :
2074+ "Invalid 'limit' parameter. It must be a positive integer." ,
2075+ } as any ,
2076+ { status : 400 } ,
2077+ )
2078+ }
2079+
2080+ const result = projects . slice ( offset , offset + limit )
2081+
2082+ await delay ( 10 )
2083+ return HttpResponse . json ( {
2084+ projects : result ,
2085+ serverTime : Date . now ( ) ,
2086+ numFound : projects . length ,
2087+ } )
2088+ } ,
2089+ ) ,
2090+ )
2091+
2092+ function LimitOffsetExample ( ) {
2093+ const {
2094+ data,
2095+ hasPreviousPage,
2096+ hasNextPage,
2097+ error,
2098+ isFetching,
2099+ isLoading,
2100+ isError,
2101+ fetchNextPage,
2102+ fetchPreviousPage,
2103+ isFetchingNextPage,
2104+ isFetchingPreviousPage,
2105+ status,
2106+ } = apiWithInfiniteScroll . useProjectsLimitOffsetInfiniteQuery (
2107+ undefined ,
2108+ {
2109+ initialPageParam : {
2110+ offset : 10 ,
2111+ limit : 10 ,
2112+ } ,
2113+ } ,
2114+ )
2115+
2116+ const [ counter , setCounter ] = useState ( 0 )
2117+
2118+ const combinedData = useMemo ( ( ) => {
2119+ return data ?. pages ?. map ( ( item ) => item ?. projects ) ?. flat ( )
2120+ } , [ data ] )
2121+
2122+ return (
2123+ < div >
2124+ < h2 > Limit and Offset Infinite Scroll</ h2 >
2125+ < button onClick = { ( ) => setCounter ( ( c ) => c + 1 ) } > Increment</ button >
2126+ < div > Counter: { counter } </ div >
2127+ { isLoading ? (
2128+ < p > Loading...</ p >
2129+ ) : isError ? (
2130+ < span > Error: { error . message } </ span >
2131+ ) : null }
2132+
2133+ < >
2134+ < div >
2135+ < button
2136+ onClick = { ( ) => fetchPreviousPage ( ) }
2137+ disabled = { ! hasPreviousPage || isFetchingPreviousPage }
2138+ >
2139+ { isFetchingPreviousPage
2140+ ? 'Loading more...'
2141+ : hasPreviousPage
2142+ ? 'Load Older'
2143+ : 'Nothing more to load' }
2144+ </ button >
2145+ </ div >
2146+ < div data-testid = "projects" >
2147+ { combinedData ?. map ( ( project , index , arr ) => {
2148+ return (
2149+ < div key = { project . id } >
2150+ < div data-testid = "project" >
2151+ < div > { `Project ${ project . id } (created at: ${ project . createdAt } )` } </ div >
2152+ </ div >
2153+ </ div >
2154+ )
2155+ } ) }
2156+ </ div >
2157+ < div >
2158+ < button
2159+ onClick = { ( ) => fetchNextPage ( ) }
2160+ disabled = { ! hasNextPage || isFetchingNextPage }
2161+ >
2162+ { isFetchingNextPage
2163+ ? 'Loading more...'
2164+ : hasNextPage
2165+ ? 'Load Newer'
2166+ : 'Nothing more to load' }
2167+ </ button >
2168+ </ div >
2169+ < div >
2170+ { isFetching && ! isFetchingPreviousPage && ! isFetchingNextPage
2171+ ? 'Background Updating...'
2172+ : null }
2173+ </ div >
2174+ </ >
2175+ </ div >
2176+ )
2177+ }
2178+
2179+ const storeRef = setupApiStore (
2180+ apiWithInfiniteScroll ,
2181+ { ...actionsReducer } ,
2182+ {
2183+ withoutTestLifecycles : true ,
2184+ } ,
2185+ )
2186+
2187+ const { takeRender, render, totalRenderCount } = createRenderStream ( {
2188+ snapshotDOM : true ,
2189+ } )
2190+
2191+ render ( < LimitOffsetExample /> , {
2192+ wrapper : storeRef . wrapper ,
2193+ } )
2194+
2195+ {
2196+ const { withinDOM } = await takeRender ( )
2197+ withinDOM ( ) . getByText ( 'Counter: 0' )
2198+ withinDOM ( ) . getByText ( 'Loading...' )
2199+ }
2200+
2201+ {
2202+ const { withinDOM } = await takeRender ( )
2203+ withinDOM ( ) . getByText ( 'Counter: 0' )
2204+ withinDOM ( ) . getByText ( 'Loading...' )
2205+ }
2206+
2207+ {
2208+ const { withinDOM } = await takeRender ( )
2209+ withinDOM ( ) . getByText ( 'Counter: 0' )
2210+
2211+ expect ( withinDOM ( ) . getAllByTestId ( 'project' ) . length ) . toBe ( 10 )
2212+ expect ( withinDOM ( ) . queryByTestId ( 'Loading...' ) ) . toBeNull ( )
2213+ }
2214+
2215+ expect ( totalRenderCount ( ) ) . toBe ( 3 )
2216+ expect ( numRequests ) . toBe ( 1 )
2217+ } )
19772218 } )
19782219
19792220 describe ( 'useMutation' , ( ) => {
0 commit comments