From f7f1dddb728fb26f2c39a383ff244b1b5fac743f Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sun, 6 Sep 2026 18:11:52 +0200 Subject: [PATCH 1/2] ref(HydrationBoundary): remove checks that are guarded by types --- .changeset/salty-hotels-turn.md | 6 ++ .../preact-query/src/HydrationBoundary.tsx | 61 +++++++---------- .../src/__tests__/HydrationBoundary.test.tsx | 52 --------------- .../react-query/src/HydrationBoundary.tsx | 65 +++++++------------ .../src/__tests__/HydrationBoundary.test.tsx | 52 --------------- 5 files changed, 55 insertions(+), 181 deletions(-) create mode 100644 .changeset/salty-hotels-turn.md diff --git a/.changeset/salty-hotels-turn.md b/.changeset/salty-hotels-turn.md new file mode 100644 index 00000000000..8cb3b5d2477 --- /dev/null +++ b/.changeset/salty-hotels-turn.md @@ -0,0 +1,6 @@ +--- +'@tanstack/preact-query': patch +'@tanstack/react-query': patch +--- + +ref(HydrationBoundary): remove checks that are guarded by types diff --git a/packages/preact-query/src/HydrationBoundary.tsx b/packages/preact-query/src/HydrationBoundary.tsx index f9cf967b069..101ebb9860d 100644 --- a/packages/preact-query/src/HydrationBoundary.tsx +++ b/packages/preact-query/src/HydrationBoundary.tsx @@ -18,7 +18,7 @@ export interface HydrationBoundaryProps { /** * The state to hydrate. */ - state: DehydratedState | null | undefined + state: DehydratedState /** * Optional. Note: unlike `hydrate`, `mutations` cannot be set here. */ @@ -113,49 +113,36 @@ export const HydrationBoundary = ({ // we throw away the fresh data for any existing ones to avoid unexpectedly // updating the UI. const hydrationQueue: DehydratedState['queries'] | undefined = useMemo(() => { - if (state) { - if (typeof state !== 'object') { - return - } - - const queryCache = client.getQueryCache() - // State is supplied from the outside and we might as well fail - // gracefully if it has the wrong shape, so while we type `queries` - // as required, we still provide a fallback. - const queries = state.queries || [] + const queryCache = client.getQueryCache() - const newQueries: DehydratedState['queries'] = [] - const existingQueries: DehydratedState['queries'] = [] - for (const dehydratedQuery of queries) { - const existingQuery = queryCache.get(dehydratedQuery.queryHash) + const newQueries: DehydratedState['queries'] = [] + const existingQueries: DehydratedState['queries'] = [] + for (const dehydratedQuery of state.queries) { + const existingQuery = queryCache.get(dehydratedQuery.queryHash) - if (!existingQuery) { - newQueries.push(dehydratedQuery) - } else { - const hydrationIsNewer = - dehydratedQuery.state.dataUpdatedAt > - existingQuery.state.dataUpdatedAt || - (dehydratedQuery.promise && - existingQuery.state.status !== 'pending' && - existingQuery.state.fetchStatus !== 'fetching' && - dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt) + if (!existingQuery) { + newQueries.push(dehydratedQuery) + } else { + const hydrationIsNewer = + dehydratedQuery.state.dataUpdatedAt > + existingQuery.state.dataUpdatedAt || + (dehydratedQuery.promise && + existingQuery.state.status !== 'pending' && + existingQuery.state.fetchStatus !== 'fetching' && + dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt) - if (hydrationIsNewer) { - existingQueries.push(dehydratedQuery) - } + if (hydrationIsNewer) { + existingQueries.push(dehydratedQuery) } } + } - if (newQueries.length > 0) { - // It's actually fine to call this with queries/state that already exists - // in the cache, or is older. hydrate() is idempotent for queries. - hydrate(client, { queries: newQueries }, optionsRef.current) - } - if (existingQueries.length > 0) { - return existingQueries - } + if (newQueries.length > 0) { + // It's actually fine to call this with queries/state that already exists + // in the cache, or is older. hydrate() is idempotent for queries. + hydrate(client, { queries: newQueries }, optionsRef.current) } - return undefined + return existingQueries.length > 0 ? existingQueries : undefined }, [client, state]) useEffect(() => { diff --git a/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx b/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx index 48802de96cf..5932cbe0ba5 100644 --- a/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx +++ b/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx @@ -322,58 +322,6 @@ describe('Preact hydration', () => { }) }) - it('should not hydrate queries if state is null', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await Promise.all( - Array.from({ length: 1000 }).map(async (_, index) => { - await vi.advanceTimersByTimeAsync(index) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - }), - ) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - - it('should not hydrate queries if state is undefined', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - it('should not hydrate queries if state is not an object', async () => { const queryClient = new QueryClient() diff --git a/packages/react-query/src/HydrationBoundary.tsx b/packages/react-query/src/HydrationBoundary.tsx index ce03eb40c83..66387e9f4a0 100644 --- a/packages/react-query/src/HydrationBoundary.tsx +++ b/packages/react-query/src/HydrationBoundary.tsx @@ -17,7 +17,7 @@ export interface HydrationBoundaryProps { /** * The state to hydrate. */ - state: DehydratedState | null | undefined + state: DehydratedState /** * Optional. Note: unlike `hydrate`, `mutations` cannot be set here. */ @@ -113,52 +113,37 @@ export const HydrationBoundary = ({ // updating the UI. const hydrationQueue: DehydratedState['queries'] | undefined = React.useMemo(() => { - if (state) { - if (typeof state !== 'object') { - return - } - - const queryCache = client.getQueryCache() - // State is supplied from the outside and we might as well fail - // gracefully if it has the wrong shape, so while we type `queries` - // as required, we still provide a fallback. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const queries = state.queries || [] + const queryCache = client.getQueryCache() - const newQueries: DehydratedState['queries'] = [] - const existingQueries: DehydratedState['queries'] = [] - for (const dehydratedQuery of queries) { - const existingQuery = queryCache.get(dehydratedQuery.queryHash) + const newQueries: DehydratedState['queries'] = [] + const existingQueries: DehydratedState['queries'] = [] + for (const dehydratedQuery of state.queries) { + const existingQuery = queryCache.get(dehydratedQuery.queryHash) - if (!existingQuery) { - newQueries.push(dehydratedQuery) - } else { - const hydrationIsNewer = - dehydratedQuery.state.dataUpdatedAt > - existingQuery.state.dataUpdatedAt || - (dehydratedQuery.promise && - existingQuery.state.status !== 'pending' && - existingQuery.state.fetchStatus !== 'fetching' && - dehydratedQuery.dehydratedAt > - existingQuery.state.dataUpdatedAt) + if (!existingQuery) { + newQueries.push(dehydratedQuery) + } else { + const hydrationIsNewer = + dehydratedQuery.state.dataUpdatedAt > + existingQuery.state.dataUpdatedAt || + (dehydratedQuery.promise && + existingQuery.state.status !== 'pending' && + existingQuery.state.fetchStatus !== 'fetching' && + dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt) - if (hydrationIsNewer) { - existingQueries.push(dehydratedQuery) - } + if (hydrationIsNewer) { + existingQueries.push(dehydratedQuery) } } + } - if (newQueries.length > 0) { - // It's actually fine to call this with queries/state that already exists - // in the cache, or is older. hydrate() is idempotent for queries. - // eslint-disable-next-line react-hooks/refs - hydrate(client, { queries: newQueries }, optionsRef.current) - } - if (existingQueries.length > 0) { - return existingQueries - } + if (newQueries.length > 0) { + // It's actually fine to call this with queries/state that already exists + // in the cache, or is older. hydrate() is idempotent for queries. + // eslint-disable-next-line react-hooks/refs + hydrate(client, { queries: newQueries }, optionsRef.current) } - return undefined + return existingQueries.length > 0 ? existingQueries : undefined }, [client, state]) React.useEffect(() => { diff --git a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx index 6418fb430f6..40543835636 100644 --- a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx +++ b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx @@ -318,58 +318,6 @@ describe('React hydration', () => { }) }) - it('should not hydrate queries if state is null', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await Promise.all( - Array.from({ length: 1000 }).map(async (_, index) => { - await vi.advanceTimersByTimeAsync(index) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - }), - ) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - - it('should not hydrate queries if state is undefined', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - it('should not hydrate queries if state is not an object', async () => { const queryClient = new QueryClient() From 537ec5562abd2438fee5da842ac0acf380b81933 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Mon, 7 Sep 2026 12:02:25 +0200 Subject: [PATCH 2/2] remove more untyped tests --- .../src/__tests__/HydrationBoundary.test.tsx | 48 ------------------- .../src/__tests__/HydrationBoundary.test.tsx | 48 ------------------- 2 files changed, 96 deletions(-) diff --git a/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx b/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx index 5932cbe0ba5..b0487a5b303 100644 --- a/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx +++ b/packages/preact-query/src/__tests__/HydrationBoundary.test.tsx @@ -322,54 +322,6 @@ describe('Preact hydration', () => { }) }) - it('should not hydrate queries if state is not an object', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - - it('should handle state without queries property gracefully', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - // https://github.com/TanStack/query/issues/8677 it('should not infinite loop when hydrating promises that resolve to errors', async () => { const originalHydrate = coreModule.hydrate diff --git a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx index 40543835636..ce8d809cc7c 100644 --- a/packages/react-query/src/__tests__/HydrationBoundary.test.tsx +++ b/packages/react-query/src/__tests__/HydrationBoundary.test.tsx @@ -318,54 +318,6 @@ describe('React hydration', () => { }) }) - it('should not hydrate queries if state is not an object', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - - it('should handle state without queries property gracefully', async () => { - const queryClient = new QueryClient() - - const hydrateSpy = vi.spyOn(coreModule, 'hydrate') - - function Page() { - return null - } - - render( - - - - - , - ) - - await vi.advanceTimersByTimeAsync(0) - expect(hydrateSpy).toHaveBeenCalledTimes(0) - - hydrateSpy.mockRestore() - queryClient.clear() - }) - // https://github.com/TanStack/query/issues/8677 it('should not infinite loop when hydrating promises that resolve to errors', async () => { const originalHydrate = coreModule.hydrate