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
18 changes: 10 additions & 8 deletions packages/toolkit/src/query/core/buildSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,13 +519,12 @@ export function buildSlice({
providedTags as FullTagDescription<string>[]
}
},
prepare:
prepareAutoBatched<
Array<{
queryCacheKey: QueryCacheKey
providedTags: readonly FullTagDescription<string>[]
}>
>(),
prepare: prepareAutoBatched<
Array<{
queryCacheKey: QueryCacheKey
providedTags: readonly FullTagDescription<string>[]
}>
>(),
},
},
extraReducers(builder) {
Expand All @@ -538,7 +537,9 @@ export function buildSlice({
)
.addMatcher(hasRehydrationInfo, (draft, action) => {
const { provided } = extractRehydrationInfo(action)!
for (const [type, incomingTags] of Object.entries(provided)) {
for (const [type, incomingTags] of Object.entries(
provided.tags ?? {},
)) {
for (const [id, cacheKeys] of Object.entries(incomingTags)) {
const subscribedQueries = ((draft.tags[type] ??= {})[
id || '__internal_without_id'
Expand All @@ -549,6 +550,7 @@ export function buildSlice({
if (!alreadySubscribed) {
subscribedQueries.push(queryCacheKey)
}
draft.keys[queryCacheKey] = provided.keys[queryCacheKey]
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/toolkit/src/query/react/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,10 +1234,10 @@ type UseInfiniteQueryStateBaseResult<
* Query is currently in "error" state.
*/
isError: false
hasNextPage: false
hasPreviousPage: false
isFetchingNextPage: false
isFetchingPreviousPage: false
hasNextPage: boolean
hasPreviousPage: boolean
isFetchingNextPage: boolean
isFetchingPreviousPage: boolean
}

type UseInfiniteQueryStateDefaultResult<
Expand Down
73 changes: 42 additions & 31 deletions packages/toolkit/src/query/tests/buildHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2282,40 +2282,51 @@ describe('hooks tests', () => {
expect(numRequests).toBe(1)
})

test('useInfiniteQuery hook does not fetch when the skip token is set', async () => {
function Pokemon() {
const [value, setValue] = useState(0)

const { isFetching } = pokemonApi.useGetInfinitePokemonInfiniteQuery(
'fire',
{
skip: value < 1,
},
)
getRenderCount = useRenderCounter()
test.each([
['skip token', true],
['skip option', false],
])(
'useInfiniteQuery hook does not fetch when skipped via %s',
async (_, useSkipToken) => {
function Pokemon() {
const [value, setValue] = useState(0)

const shouldFetch = value > 0

const arg = shouldFetch || !useSkipToken ? 'fire' : skipToken
const skip = useSkipToken ? undefined : shouldFetch ? undefined : true

const { isFetching } = pokemonApi.useGetInfinitePokemonInfiniteQuery(
arg,
{
skip,
},
)
getRenderCount = useRenderCounter()

return (
<div>
<div data-testid="isFetching">{String(isFetching)}</div>
<button onClick={() => setValue((val) => val + 1)}>
Increment value
</button>
</div>
)
}
return (
<div>
<div data-testid="isFetching">{String(isFetching)}</div>
<button onClick={() => setValue((val) => val + 1)}>
Increment value
</button>
</div>
)
}

render(<Pokemon />, { wrapper: storeRef.wrapper })
expect(getRenderCount()).toBe(1)
render(<Pokemon />, { wrapper: storeRef.wrapper })
expect(getRenderCount()).toBe(1)

await waitFor(() =>
expect(screen.getByTestId('isFetching').textContent).toBe('false'),
)
fireEvent.click(screen.getByText('Increment value'))
await waitFor(() =>
expect(screen.getByTestId('isFetching').textContent).toBe('true'),
)
expect(getRenderCount()).toBe(2)
})
await waitFor(() =>
expect(screen.getByTestId('isFetching').textContent).toBe('false'),
)
fireEvent.click(screen.getByText('Increment value'))
await waitFor(() =>
expect(screen.getByTestId('isFetching').textContent).toBe('true'),
)
expect(getRenderCount()).toBe(2)
},
)
})

describe('useMutation', () => {
Expand Down
127 changes: 127 additions & 0 deletions packages/toolkit/src/query/tests/buildInitiate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,130 @@ describe('calling initiate should have resulting queryCacheKey match baseQuery q
)
})
})

describe('getRunningQueryThunk with multiple stores', () => {
test('should isolate running queries between different store instances using the same API', async () => {
// Create a shared API instance
const sharedApi = createApi({
baseQuery: fakeBaseQuery(),
endpoints: (build) => ({
testQuery: build.query<string, string>({
async queryFn(arg) {
// Add delay to ensure queries are running when we check
await new Promise((resolve) => setTimeout(resolve, 50))
return { data: `result-${arg}` }
},
}),
}),
})

// Create two separate stores using the same API instance
const store1 = setupApiStore(sharedApi, undefined, {
withoutTestLifecycles: true,
}).store
const store2 = setupApiStore(sharedApi, undefined, {
withoutTestLifecycles: true,
}).store

// Start queries on both stores
const query1Promise = store1.dispatch(
sharedApi.endpoints.testQuery.initiate('arg1'),
)
const query2Promise = store2.dispatch(
sharedApi.endpoints.testQuery.initiate('arg2'),
)

// Verify that getRunningQueryThunk returns the correct query for each store
const runningQuery1 = store1.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg1'),
)
const runningQuery2 = store2.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg2'),
)

// Each store should only see its own running query
expect(runningQuery1).toBeDefined()
expect(runningQuery2).toBeDefined()
expect(runningQuery1?.requestId).toBe(query1Promise.requestId)
expect(runningQuery2?.requestId).toBe(query2Promise.requestId)

// Cross-store queries should not be visible
const crossQuery1 = store1.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg2'),
)
const crossQuery2 = store2.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg1'),
)

expect(crossQuery1).toBeUndefined()
expect(crossQuery2).toBeUndefined()

// Wait for queries to complete
await Promise.all([query1Promise, query2Promise])

// After completion, getRunningQueryThunk should return undefined for both stores
const completedQuery1 = store1.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg1'),
)
const completedQuery2 = store2.dispatch(
sharedApi.util.getRunningQueryThunk('testQuery', 'arg2'),
)

expect(completedQuery1).toBeUndefined()
expect(completedQuery2).toBeUndefined()
})

test('should handle same query args on different stores independently', async () => {
// Create a shared API instance
const sharedApi = createApi({
baseQuery: fakeBaseQuery(),
endpoints: (build) => ({
sameArgQuery: build.query<string, string>({
async queryFn(arg) {
await new Promise((resolve) => setTimeout(resolve, 50))
return { data: `result-${arg}-${Math.random()}` }
},
}),
}),
})

// Create two separate stores
const store1 = setupApiStore(sharedApi, undefined, {
withoutTestLifecycles: true,
}).store
const store2 = setupApiStore(sharedApi, undefined, {
withoutTestLifecycles: true,
}).store

// Start the same query on both stores
const sameArg = 'shared-arg'
const query1Promise = store1.dispatch(
sharedApi.endpoints.sameArgQuery.initiate(sameArg),
)
const query2Promise = store2.dispatch(
sharedApi.endpoints.sameArgQuery.initiate(sameArg),
)

// Both stores should see their own running query with the same cache key
const runningQuery1 = store1.dispatch(
sharedApi.util.getRunningQueryThunk('sameArgQuery', sameArg),
)
const runningQuery2 = store2.dispatch(
sharedApi.util.getRunningQueryThunk('sameArgQuery', sameArg),
)

expect(runningQuery1).toBeDefined()
expect(runningQuery2).toBeDefined()
expect(runningQuery1?.requestId).toBe(query1Promise.requestId)
expect(runningQuery2?.requestId).toBe(query2Promise.requestId)

// The request IDs should be different even though the cache key is the same
expect(runningQuery1?.requestId).not.toBe(runningQuery2?.requestId)

// But the cache keys should be the same
expect(runningQuery1?.queryCacheKey).toBe(runningQuery2?.queryCacheKey)

// Wait for completion
await Promise.all([query1Promise, query2Promise])
})
})
27 changes: 26 additions & 1 deletion packages/toolkit/src/query/tests/buildSlice.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { createSlice } from '@reduxjs/toolkit'
import { createSlice, createAction } from '@reduxjs/toolkit'
import type { CombinedState } from '@reduxjs/toolkit/query'
import { createApi } from '@reduxjs/toolkit/query'
import { delay } from 'msw'
import { setupApiStore } from '../../tests/utils/helpers'

let shouldApiResponseSuccess = true

const rehydrateAction = createAction<{ api: CombinedState<any, any, any> }>(
'persist/REHYDRATE',
)

const baseQuery = (args?: any) => ({ data: args })
const api = createApi({
baseQuery,
Expand All @@ -17,6 +22,12 @@ const api = createApi({
providesTags: (result) => (result?.success ? ['SUCCEED'] : ['FAILED']),
}),
}),
extractRehydrationInfo(action, { reducerPath }) {
if (rehydrateAction.match(action)) {
return action.payload?.[reducerPath]
}
return undefined
},
})
const { getUser } = api.endpoints

Expand Down Expand Up @@ -114,6 +125,20 @@ describe('buildSlice', () => {
api.util.selectInvalidatedBy(storeRef.store.getState(), ['FAILED']),
).toHaveLength(1)
})

it('handles extractRehydrationInfo correctly', async () => {
await storeRef.store.dispatch(getUser.initiate(1))
await storeRef.store.dispatch(getUser.initiate(2))

const stateWithUser = storeRef.store.getState()

storeRef.store.dispatch(api.util.resetApiState())

storeRef.store.dispatch(rehydrateAction({ api: stateWithUser.api }))

const rehydratedState = storeRef.store.getState()
expect(rehydratedState).toEqual(stateWithUser)
})
})

describe('`merge` callback', () => {
Expand Down
Loading