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