diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx
index d807ef72ae..83cc5f4cc7 100644
--- a/packages/toolkit/src/query/tests/buildHooks.test.tsx
+++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx
@@ -1906,6 +1906,57 @@ describe('hooks tests', () => {
await screen.findByText(/isUninitialized/i)
expect(countObjectKeys(storeRef.store.getState().api.queries)).toBe(0)
})
+
+ // Regression test for issue #5271: useLazyQuery isFetching stays false when trigger args contain undefined values
+ test('useLazyQuery updates isFetching when trigger args contain undefined values', async () => {
+ const user = userEvent.setup()
+
+ function User() {
+ const [getUser, { isFetching, data, isSuccess }] =
+ api.endpoints.getUser.useLazyQuery()
+
+ return (
+
+
{String(isFetching)}
+
{data ? JSON.stringify(data) : 'no data'}
+
+
+
+ )
+ }
+
+ render(, { wrapper: storeRef.wrapper })
+
+ // First call with { filterId: '1' }
+ await user.click(screen.getByRole('button', { name: 'Load Filtered' }))
+ await waitFor(() =>
+ expect(screen.getByTestId('isFetching').textContent).toBe('true'),
+ )
+ await waitFor(() =>
+ expect(screen.getByTestId('isFetching').textContent).toBe('false'),
+ )
+ await waitFor(() =>
+ expect(screen.getByTestId('data').textContent).toContain('Alice'),
+ )
+
+ // Second call with { triggeredAddress: undefined, page: 1 }
+ // This should also update isFetching and data (issue #5271)
+ await user.click(screen.getByRole('button', { name: 'Load Full List' }))
+ await waitFor(() =>
+ expect(screen.getByTestId('isFetching').textContent).toBe('true'),
+ )
+ await waitFor(() =>
+ expect(screen.getByTestId('isFetching').textContent).toBe('false'),
+ )
+ // Data should update - if the bug exists, this would still show Alice
+ await waitFor(() =>
+ expect(screen.getByTestId('data').textContent).toContain('Alice'),
+ )
+ })
})
describe('useInfiniteQuery', () => {
diff --git a/packages/toolkit/src/query/tests/copyWithStructuralSharing.test.ts b/packages/toolkit/src/query/tests/copyWithStructuralSharing.test.ts
index 889414d9de..972c5e17b9 100644
--- a/packages/toolkit/src/query/tests/copyWithStructuralSharing.test.ts
+++ b/packages/toolkit/src/query/tests/copyWithStructuralSharing.test.ts
@@ -1,5 +1,36 @@
import { copyWithStructuralSharing } from '@reduxjs/toolkit/query'
+// Test for preserving keys with undefined values (issue #5271)
+test('preserves keys with undefined values', () => {
+ const objA = { page: 1 }
+ const objB = { triggeredAddress: undefined, page: 1 }
+
+ // These should be treated as different objects
+ const newCopy = copyWithStructuralSharing(objA, objB)
+ expect(newCopy).not.toBe(objA)
+ expect(newCopy).toStrictEqual(objB)
+ expect(Object.getOwnPropertyNames(newCopy)).toContain('triggeredAddress')
+})
+
+test('preserves nested keys with undefined values', () => {
+ const objA = { filter: { page: 1 } }
+ const objB = { filter: { triggeredAddress: undefined, page: 1 } }
+
+ const newCopy = copyWithStructuralSharing(objA, objB)
+ expect(newCopy).not.toBe(objA)
+ expect(newCopy.filter).not.toBe(objA.filter)
+ expect(Object.getOwnPropertyNames(newCopy.filter)).toContain('triggeredAddress')
+})
+
+test('returns same object when both have same undefined keys', () => {
+ const objA = { triggeredAddress: undefined, page: 1 }
+ const objB = { triggeredAddress: undefined, page: 1 }
+
+ const newCopy = copyWithStructuralSharing(objA, objB)
+ expect(newCopy).toBe(objA)
+ expect(newCopy).toStrictEqual(objB)
+})
+
test('equal object from JSON Object', () => {
const json = JSON.stringify({
a: { b: { c: { d: 1, e: '2', f: true }, g: false }, h: null },
diff --git a/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts b/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
index 11e6cecdd3..ba26bc3b8d 100644
--- a/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
+++ b/packages/toolkit/src/query/utils/copyWithStructuralSharing.ts
@@ -14,8 +14,10 @@ export function copyWithStructuralSharing(oldObj: any, newObj: any): any {
) {
return newObj
}
- const newKeys = Object.keys(newObj)
- const oldKeys = Object.keys(oldObj)
+ // Use getOwnPropertyNames to include keys with undefined values
+ // This is important for query args that may contain explicit undefined values
+ const newKeys = Object.getOwnPropertyNames(newObj)
+ const oldKeys = Object.getOwnPropertyNames(oldObj)
let isSameObject = newKeys.length === oldKeys.length
const mergeObj: any = Array.isArray(newObj) ? [] : {}