From 29bcca3bdf52c57e6a562838ea16133458218e19 Mon Sep 17 00:00:00 2001 From: Reza Rahemtola Date: Fri, 18 Sep 2026 00:17:53 +0200 Subject: [PATCH] fix(preact-query): do not go into optimistic fetching state when not subscribed --- .../preact-unsubscribed-optimistic-state.md | 5 + .../unsubscribed-optimistic.test.tsx | 92 +++++++++++++++++++ packages/preact-query/src/useBaseQuery.ts | 7 +- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 .changeset/preact-unsubscribed-optimistic-state.md create mode 100644 packages/preact-query/src/__tests__/unsubscribed-optimistic.test.tsx diff --git a/.changeset/preact-unsubscribed-optimistic-state.md b/.changeset/preact-unsubscribed-optimistic-state.md new file mode 100644 index 00000000000..b445ff975af --- /dev/null +++ b/.changeset/preact-unsubscribed-optimistic-state.md @@ -0,0 +1,5 @@ +--- +'@tanstack/preact-query': patch +--- + +fix(preact-query): do not go into optimistic fetching state when not subscribed diff --git a/packages/preact-query/src/__tests__/unsubscribed-optimistic.test.tsx b/packages/preact-query/src/__tests__/unsubscribed-optimistic.test.tsx new file mode 100644 index 00000000000..588433bd288 --- /dev/null +++ b/packages/preact-query/src/__tests__/unsubscribed-optimistic.test.tsx @@ -0,0 +1,92 @@ +import { queryKey } from '@tanstack/query-test-utils' +import { fireEvent } from '@testing-library/preact' +import { useState } from 'preact/hooks' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { QueryCache, QueryClient, useQuery } from '..' +import { renderWithClient } from './utils' + +describe('useQuery unsubscribed optimistic state', () => { + let queryClient: QueryClient + + beforeEach(() => { + queryClient = new QueryClient({ queryCache: new QueryCache() }) + vi.useFakeTimers() + }) + + afterEach(() => { + queryClient.clear() + vi.useRealTimers() + }) + + it('should not show fetching on re-render when unsubscribed', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + function Page() { + const [subscribed, setSubscribed] = useState(true) + const [other, setOther] = useState(false) + const query = useQuery({ + queryKey: key, + queryFn, + subscribed, + }) + + return ( +
+ isFetching: {String(query.isFetching)} + fetchStatus: {query.fetchStatus} + + +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await vi.advanceTimersByTimeAsync(0) + rendered.getByText('isFetching: false') + rendered.getByText('fetchStatus: idle') + + fireEvent.click(rendered.getByRole('button', { name: 'unsubscribe' })) + await vi.advanceTimersByTimeAsync(0) + + expect(queryFn).toHaveBeenCalledTimes(1) + expect( + queryClient.getQueryCache().find({ queryKey: key })!.observers.length, + ).toBe(0) + + // re-render while unsubscribed + fireEvent.click(rendered.getByRole('button', { name: 'rerender' })) + await vi.advanceTimersByTimeAsync(0) + + rendered.getByText('isFetching: false') + rendered.getByText('fetchStatus: idle') + }) + + it('should not show fetching when unsubscribed from the start', async () => { + const key = queryKey() + const queryFn = vi.fn(() => Promise.resolve('data')) + + function Page() { + const query = useQuery({ + queryKey: key, + queryFn, + subscribed: false, + }) + + return ( +
+ isFetching: {String(query.isFetching)} + fetchStatus: {query.fetchStatus} +
+ ) + } + + const rendered = renderWithClient(queryClient, ) + await vi.advanceTimersByTimeAsync(0) + + expect(queryFn).toHaveBeenCalledTimes(0) + rendered.getByText('isFetching: false') + rendered.getByText('fetchStatus: idle') + }) +}) diff --git a/packages/preact-query/src/useBaseQuery.ts b/packages/preact-query/src/useBaseQuery.ts index e5e37cbc52b..c05aa8aa29c 100644 --- a/packages/preact-query/src/useBaseQuery.ts +++ b/packages/preact-query/src/useBaseQuery.ts @@ -71,9 +71,12 @@ export function useBaseQuery< } // Make sure results are optimistically set in fetching state before subscribing or updating options + const subscribed = options.subscribed !== false defaultedOptions._optimisticResults = isRestoring ? 'isRestoring' - : 'optimistic' + : subscribed + ? 'optimistic' + : undefined ensureSuspenseTimers(defaultedOptions) ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query) @@ -91,7 +94,7 @@ export function useBaseQuery< // note: this must be called before useSyncExternalStore const result = observer.getOptimisticResult(defaultedOptions) - const shouldSubscribe = !isRestoring && options.subscribed !== false + const shouldSubscribe = !isRestoring && subscribed useSyncExternalStore( useCallback( (onStoreChange) => {