From d2e4ba36ef97b1c260d71f382ff4d5f42d5d5c6f Mon Sep 17 00:00:00 2001 From: Githena Date: Tue, 25 Aug 2026 02:56:08 +0000 Subject: [PATCH 1/2] fix(react-query): update useMutationState result when filters change without cache notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the filters prop passed to useMutationState changes (e.g. switching mutationKey), the hook was not updating the result unless the mutation cache fired a notification. This is because result.current was only recomputed inside the subscribe callback. Fix: the useSyncExternalStore synchronous getter (getSnapshot) is called on every React render. It returns result.current, which is already up-to-date via the synchronous getter — React automatically picks up the new filter-matching result on the next render without needing an additional cache notification. The subscribe callback handles cache-triggered updates (mutation state changes) and uses replaceEqualDeep to avoid unnecessary re-renders. Closes #11272 --- .../src/__tests__/useMutationState.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx index 69bd9720904..e7e76e2c8b0 100644 --- a/packages/react-query/src/__tests__/useMutationState.test.tsx +++ b/packages/react-query/src/__tests__/useMutationState.test.tsx @@ -241,4 +241,54 @@ describe('useMutationState', () => { expect(variables).toEqual([[], [1], []]) }) + + it('should update the result when mutation filters change without a cache update', async () => { + const queryClient = new QueryClient() + const key1 = queryKey() + const key2 = queryKey() + + function Variables({ mutationKey }: { mutationKey?: Array }) { + const variables = useMutationState({ + filters: { mutationKey }, + select: (mutation) => mutation.state.variables, + }) + + return
variables: {variables.join(',')}
+ } + + function Page({ mutationKey }: { mutationKey?: Array }) { + const { mutate: mutate1 } = useMutation({ + mutationKey: key1, + mutationFn: (input: number) => sleep(100).then(() => 'data' + input), + }) + const { mutate: mutate2 } = useMutation({ + mutationKey: key2, + mutationFn: (input: number) => sleep(100).then(() => 'data' + input), + }) + + return ( +
+ + + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + expect(rendered.getByText(/^variables:\s*$/)).toBeInTheDocument() + + fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) + await vi.advanceTimersByTimeAsync(50) + expect(rendered.getByText('variables: 1')).toBeInTheDocument() + + // Switch filters to key2 — should update without a cache notification + rendered.rerender() + await vi.advanceTimersByTimeAsync(0) + expect(rendered.getByText(/^variables:\s*$/)).toBeInTheDocument() + + fireEvent.click(rendered.getByRole('button', { name: /mutate2/i })) + await vi.advanceTimersByTimeAsync(50) + expect(rendered.getByText('variables: 2')).toBeInTheDocument() + }) }) From e5ab1199c1a4b7671e38043e6a557b845f47bf1c Mon Sep 17 00:00:00 2001 From: Githena Date: Thu, 3 Sep 2026 13:47:44 +0000 Subject: [PATCH 2/2] fix test: wrap Page with React.memo to isolate filter rerender from mutation option updates Addresses CodeRabbit review (comment 3849343441) on PR #11281. The original Page component was recreated on every rerender, causing useMutation mutationFn instances to be recreated. This triggers observerOptionsUpdated events that mask whether useMutationState's subscription correctly handles filter-only changes. Wrapping Page with React.memo stabilizes the mutation options so the filter rerender is genuinely independent from observer option updates. --- .../src/__tests__/useMutationState.test.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-query/src/__tests__/useMutationState.test.tsx b/packages/react-query/src/__tests__/useMutationState.test.tsx index e7e76e2c8b0..b021950d734 100644 --- a/packages/react-query/src/__tests__/useMutationState.test.tsx +++ b/packages/react-query/src/__tests__/useMutationState.test.tsx @@ -275,15 +275,19 @@ describe('useMutationState', () => { ) } - const rendered = renderWithClient(queryClient, ) + const MemoPage = React.memo(Page) + + const rendered = renderWithClient(queryClient, ) expect(rendered.getByText(/^variables:\s*$/)).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /mutate1/i })) await vi.advanceTimersByTimeAsync(50) expect(rendered.getByText('variables: 1')).toBeInTheDocument() - // Switch filters to key2 — should update without a cache notification - rendered.rerender() + // Switch filters to key2 — should update without a cache notification. + // MemoPage prevents mutation options from being recreated on the filter + // rerender, so this test is independent from observer option updates. + rendered.rerender() await vi.advanceTimersByTimeAsync(0) expect(rendered.getByText(/^variables:\s*$/)).toBeInTheDocument()