diff --git a/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx index 52e5d745cb..7abbf8b16b 100644 --- a/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx +++ b/packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx @@ -364,7 +364,7 @@ describe('PersistQueryClientProvider', () => { return (
-

data: {state.data ?? 'null'}

+

data: {state.data}

fetchStatus: {state.fetchStatus}

) @@ -381,7 +381,7 @@ describe('PersistQueryClientProvider', () => { )) - expect(screen.getByText('data: null')).toBeInTheDocument() + expect(screen.getByText('data:')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) expect(screen.getByText('data: hydrated')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(10) diff --git a/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx b/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx index 5d212f7edf..92b18e3259 100644 --- a/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/infiniteQueryOptions.test-d.tsx @@ -52,7 +52,7 @@ describe('infiniteQueryOptions', () => { }) expectTypeOf(() => useInfiniteQuery(() => options).data).toEqualTypeOf< - () => InfiniteData<{ wow: boolean }, unknown> | undefined + () => InfiniteData<{ wow: boolean }, unknown> >() expectTypeOf(options).toExtend< diff --git a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx index 299536290e..791d9ed38d 100644 --- a/packages/solid-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/solid-query/src/__tests__/queryOptions.test-d.tsx @@ -37,7 +37,7 @@ describe('queryOptions', () => { }) const { data } = useQuery(() => options) - expectTypeOf(data).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf() }) it('should work when passed to fetchQuery', async () => { const options = queryOptions({ diff --git a/packages/solid-query/src/__tests__/suspense.test.tsx b/packages/solid-query/src/__tests__/suspense.test.tsx index bc30409acf..c6d6cbaabf 100644 --- a/packages/solid-query/src/__tests__/suspense.test.tsx +++ b/packages/solid-query/src/__tests__/suspense.test.tsx @@ -1,6 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent } from '@solidjs/testing-library' -import { Errored, Loading, createRenderEffect, createSignal } from 'solid-js' +import { + Errored, + Loading, + createRenderEffect, + createSignal, + deep, +} from 'solid-js' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryCache, QueryClient, useInfiniteQuery, useQuery } from '..' import { renderWithClient } from './utils' @@ -37,14 +43,14 @@ describe("useQuery's in Loading mode", () => { })) createRenderEffect( - () => state, + () => deep(state), (s) => { - states.push({ ...s }) + states.push(s) }, ) createRenderEffect( - () => [{ ...state }, () => key], + () => [deep(state), () => key], () => { renders++ }, @@ -92,16 +98,16 @@ describe("useQuery's in Loading mode", () => { })) createRenderEffect( - () => state, + () => deep(state), (s) => { - states.push({ ...s }) + states.push(s) }, ) return (
- data: {state.data?.pages.join(',')} + data: {state.data.pages.join(',')}
) } diff --git a/packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx index e1cc1dbdfd..57cb0c6b88 100644 --- a/packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/solid-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -76,9 +76,7 @@ describe('useInfiniteQuery', () => { getNextPageParam: () => undefined, })) - expectTypeOf(data).toEqualTypeOf< - InfiniteData | undefined - >() + expectTypeOf(data).toEqualTypeOf>() }) }) @@ -95,7 +93,7 @@ describe('useInfiniteQuery', () => { // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< - InfiniteData | undefined + InfiniteData >() }) @@ -113,7 +111,7 @@ describe('useInfiniteQuery', () => { }, })) - expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>() + expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected'>() }) }) @@ -153,7 +151,7 @@ describe('useInfiniteQuery', () => { // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< - InfiniteData | undefined + InfiniteData >() }) }) @@ -199,7 +197,7 @@ describe('useInfiniteQuery', () => { // TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now expectTypeOf(infiniteQuery.data).toEqualTypeOf< - InfiniteData | undefined + InfiniteData >() }) }) diff --git a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx index 939b30ef8f..d3548b744c 100644 --- a/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useInfiniteQuery.test.tsx @@ -8,7 +8,9 @@ import { Switch, createRenderEffect, createSignal, + deep, snapshot, + untrack, } from 'solid-js' import { queryKey, sleep } from '@tanstack/query-test-utils' import { @@ -63,7 +65,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -216,18 +218,10 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ - data: state.data, - isFetching: state.isFetching, - isFetchingNextPage: state.isFetchingNextPage, - isSuccess: state.isSuccess, - isPlaceholderData: state.isPlaceholderData, - }), + () => deep(state), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isSuccess: state.isSuccess, @@ -240,7 +234,7 @@ describe('useInfiniteQuery', () => {
-
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {String(state.isFetching)}
) @@ -330,7 +324,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => { renderCount++ - return { status: state.status, data: state.data } + return { status: state.status, dataUpdatedAt: state.dataUpdatedAt } }, () => { states.push(snapshot(state) as any) @@ -382,7 +376,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), (s) => { states.push(s) }, @@ -430,12 +424,10 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isSuccess: state.isSuccess, }) }, @@ -444,7 +436,7 @@ describe('useInfiniteQuery', () => { return (
-
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {state.isFetching}
) @@ -500,7 +492,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, hasNextPage: state.hasNextPage, hasPreviousPage: state.hasPreviousPage, isFetching: state.isFetching, @@ -510,9 +502,7 @@ describe('useInfiniteQuery', () => { }), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, hasNextPage: state.hasNextPage, hasPreviousPage: state.hasPreviousPage, isFetching: state.isFetching, @@ -594,7 +584,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isRefetching: state.isRefetching, @@ -602,9 +592,7 @@ describe('useInfiniteQuery', () => { }), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isRefetching: state.isRefetching, @@ -620,7 +608,7 @@ describe('useInfiniteQuery', () => { fetchPreviousPage -
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {String(state.isFetching)}
) @@ -736,7 +724,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -747,9 +735,7 @@ describe('useInfiniteQuery', () => { }), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -771,7 +757,7 @@ describe('useInfiniteQuery', () => { > refetch -
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {String(state.isFetching)}
) @@ -859,7 +845,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -870,9 +856,7 @@ describe('useInfiniteQuery', () => { }), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -887,7 +871,7 @@ describe('useInfiniteQuery', () => { return (
-
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {String(state.isFetching)}
) @@ -975,7 +959,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -986,9 +970,7 @@ describe('useInfiniteQuery', () => { }), () => { states.push({ - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchNextPageError: state.isFetchNextPageError, isFetchingNextPage: state.isFetchingNextPage, @@ -1005,7 +987,7 @@ describe('useInfiniteQuery', () => { -
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
isFetching: {String(state.isFetching)}
) @@ -1090,8 +1072,9 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ + data: untrack(() => state.data), hasNextPage: state.hasNextPage, - data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isSuccess: state.isSuccess, @@ -1319,7 +1302,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1426,7 +1409,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ hasNextPage: state.hasNextPage, - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isSuccess: state.isSuccess, @@ -1434,9 +1417,7 @@ describe('useInfiniteQuery', () => { () => { states.push({ hasNextPage: state.hasNextPage, - data: state.data - ? JSON.parse(JSON.stringify(state.data)) - : undefined, + data: state.data, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isSuccess: state.isSuccess, @@ -1524,7 +1505,7 @@ describe('useInfiniteQuery', () => { createRenderEffect( () => ({ hasNextPage: state.hasNextPage, - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isFetchingNextPage: state.isFetchingNextPage, isSuccess: state.isSuccess, @@ -1599,7 +1580,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1647,7 +1628,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1695,7 +1676,7 @@ describe('useInfiniteQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1746,7 +1727,7 @@ describe('useInfiniteQuery', () => { return (
-
data: {state.data?.pages.join(',') ?? 'null'}
+
data: {state.data.pages.join(',')}
hasNextPage: {state.hasNextPage ? 'true' : 'false'}
) @@ -1796,7 +1777,7 @@ describe('useInfiniteQuery', () => { fallback={ <>
Data:
- + {(page, i) => (
@@ -1941,7 +1922,7 @@ describe('useInfiniteQuery', () => { fallback={ <>
Data:
- + {(page, i) => (
@@ -2088,7 +2069,7 @@ describe('useInfiniteQuery', () => { ) return (
-

Status: {state.data?.pages[0]}

+

Status: {state.data.pages[0]}

) } @@ -2120,7 +2101,7 @@ describe('useInfiniteQuery', () => { ) return (
-

Status: {state.data?.pages[0]}

+

Status: {state.data.pages[0]}

) } diff --git a/packages/solid-query/src/__tests__/useQueries.test-d.tsx b/packages/solid-query/src/__tests__/useQueries.test-d.tsx index eaa0a42d45..617ec56b36 100644 --- a/packages/solid-query/src/__tests__/useQueries.test-d.tsx +++ b/packages/solid-query/src/__tests__/useQueries.test-d.tsx @@ -5,14 +5,17 @@ import { queryOptions, useQueries } from '..' import { QueryClient } from '../QueryClient' import type * as QueryCore from '@tanstack/query-core' import type { OmitKeyof } from '@tanstack/query-core' -import type { - QueryFunction, - QueryFunctionContext, - QueryKey, - UseQueryResult, -} from '..' +import type { QueryFunction, QueryFunctionContext, QueryKey } from '..' import type { QueryOptions } from '../types' +// useQueries results are a plain reactive store with no resource backing +// (reads never suspend), so unlike useQuery its `data` stays nullable — +// assert against the raw query-core observer result here. +type UseQueryResult< + TData = unknown, + TError = QueryCore.DefaultError, +> = QueryCore.QueryObserverResult + describe('useQueries', () => { it('TData should have undefined in the union even when initialData is provided as an object', () => { const query1 = { diff --git a/packages/solid-query/src/__tests__/useQuery.test-d.tsx b/packages/solid-query/src/__tests__/useQuery.test-d.tsx index 278f4710b8..f37b7caa0b 100644 --- a/packages/solid-query/src/__tests__/useQuery.test-d.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test-d.tsx @@ -16,7 +16,7 @@ describe('useQuery', () => { queryKey: key, queryFn: () => 'test', })) - expectTypeOf(fromQueryFn.data).toEqualTypeOf() + expectTypeOf(fromQueryFn.data).toEqualTypeOf() expectTypeOf(fromQueryFn.error).toEqualTypeOf() // it should be possible to specify the result type @@ -24,7 +24,7 @@ describe('useQuery', () => { queryKey: key, queryFn: () => 'test', })) - expectTypeOf(withResult.data).toEqualTypeOf() + expectTypeOf(withResult.data).toEqualTypeOf() expectTypeOf(withResult.error).toEqualTypeOf() // it should be possible to specify the error type @@ -32,7 +32,7 @@ describe('useQuery', () => { queryKey: key, queryFn: () => 'test', })) - expectTypeOf(withError.data).toEqualTypeOf() + expectTypeOf(withError.data).toEqualTypeOf() expectTypeOf(withError.error).toEqualTypeOf() // it should provide the result type in the configuration @@ -46,12 +46,12 @@ describe('useQuery', () => { queryKey: key, queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)), })) - expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b'>() const unionTypeAsync = useQuery<'a' | 'b'>(() => ({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), })) - expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b'>() // should error when the query function result does not match with the specified type // @ts-expect-error @@ -66,16 +66,14 @@ describe('useQuery', () => { queryKey: key, queryFn: () => queryFn(), })) - expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf() + expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf() const fromGenericOptionsQueryFn = useQuery(() => ({ queryKey: key, queryFn: () => queryFn(), })) - expectTypeOf(fromGenericOptionsQueryFn.data).toEqualTypeOf< - string | undefined - >() + expectTypeOf(fromGenericOptionsQueryFn.data).toEqualTypeOf() expectTypeOf(fromGenericOptionsQueryFn.error).toEqualTypeOf() type MyData = number @@ -133,7 +131,7 @@ describe('useQuery', () => { ...options, })) const test = useWrappedQuery([''], () => Promise.resolve('1')) - expectTypeOf(test.data).toEqualTypeOf() + expectTypeOf(test.data).toEqualTypeOf() // handles wrapped queries with custom fetcher passed directly to useQuery const useWrappedFuncStyleQuery = < @@ -153,7 +151,7 @@ describe('useQuery', () => { const testFuncStyle = useWrappedFuncStyleQuery([''], () => Promise.resolve(true), ) - expectTypeOf(testFuncStyle.data).toEqualTypeOf() + expectTypeOf(testFuncStyle.data).toEqualTypeOf() describe('initialData', () => { describe('Config object overload', () => { @@ -188,16 +186,20 @@ describe('useQuery', () => { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) - it('TData should have undefined in the union when initialData is NOT provided', () => { + it('TData should be non-nullable even when initialData is NOT provided (reads suspend until data is ready)', () => { const { data } = useQuery(() => ({ queryKey: queryKey(), queryFn: () => ({ wow: true }), })) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { + // The maybe-undefined initialData function infers + // TData = { wow: boolean } | undefined through the defined-initialData + // overload, so the undefined here comes from TData itself — not from + // the (suspending, non-nullable) result wrapper. const { data } = useQuery(() => ({ queryKey: queryKey(), queryFn: () => ({ wow: true }), @@ -219,13 +221,13 @@ describe('useQuery', () => { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) - it('TData should have undefined in the union when initialData is NOT provided', () => { + it('TData should be non-nullable even when initialData is NOT provided (reads suspend until data is ready)', () => { const { data } = useQuery(() => ({ queryKey: queryKey(), queryFn: () => ({ wow: true }), })) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) }) @@ -240,13 +242,13 @@ describe('useQuery', () => { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) - it('TData should have undefined in the union when initialData is NOT provided', () => { + it('TData should be non-nullable even when initialData is NOT provided (reads suspend until data is ready)', () => { const { data } = useQuery(() => ({ queryKey: queryKey(), queryFn: () => ({ wow: true }), })) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) }) }) @@ -292,7 +294,7 @@ describe('useQuery', () => { // Regression guard: this call must compile. With the previous // hand-rolled NoInfer, `data` failed to flow back into the generic // indexed-access parameter `DataTypeToEntity[TDataType]`. - return data ? getLabel(props.dataType, data) : null + return getLabel(props.dataType, data) } expectTypeOf(Test).toBeFunction() diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx index 1b43f61a4a..cc2127ff0a 100644 --- a/packages/solid-query/src/__tests__/useQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test.tsx @@ -17,6 +17,7 @@ import { createRenderEffect, createSignal, createTrackedEffect, + deep, reconcile, snapshot, untrack, @@ -66,7 +67,7 @@ describe('useQuery', () => { return (
-

{state.data ?? 'default'}

+

{state.isPending ? 'default' : state.data}

) } @@ -95,7 +96,7 @@ describe('useQuery', () => { createRenderEffect( () => ({ status: state.status, - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, }), () => { @@ -104,10 +105,12 @@ describe('useQuery', () => { ) if (state.isPending) { - expectTypeOf(state.data).toEqualTypeOf() + // `data` is typed non-nullable in every status variant: reads + // suspend to the nearest Loading boundary until the value exists. + expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() } else if (state.isLoadingError) { - expectTypeOf(state.data).toEqualTypeOf() + expectTypeOf(state.data).toEqualTypeOf() expectTypeOf(state.error).toEqualTypeOf() } else { expectTypeOf(state.data).toEqualTypeOf() @@ -343,7 +346,7 @@ describe('useQuery', () => { queryFn: () => sleep(10).then(() => 'data'), })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -499,7 +502,7 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -549,7 +552,7 @@ describe('useQuery', () => { gcTime: 0, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -613,7 +616,7 @@ describe('useQuery', () => { refetchOnMount: false, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -647,7 +650,7 @@ describe('useQuery', () => { refetchOnMount: false, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -678,7 +681,7 @@ describe('useQuery', () => { select: (data) => data.name, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -710,7 +713,7 @@ describe('useQuery', () => { select: (data) => data.name, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -742,7 +745,7 @@ describe('useQuery', () => { select: (data) => data.name, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -776,7 +779,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -818,7 +821,11 @@ describe('useQuery', () => { }, })) createRenderEffect( - () => ({ status: state.status, data: state.data, error: state.error }), + () => ({ + status: state.status, + dataUpdatedAt: state.dataUpdatedAt, + error: state.error, + }), () => { const s = snapshot(state) if (s.status === 'pending') @@ -855,7 +862,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -873,7 +880,7 @@ describe('useQuery', () => { return (
-

{state.data ?? null}

+

{state.data}

) } @@ -904,7 +911,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -951,9 +958,7 @@ describe('useQuery', () => { })) createTrackedEffect(() => { - if (state.data) { - states.push(state.data) - } + states.push(state.data) }) const refetch = untrack(() => state.refetch) @@ -1030,12 +1035,12 @@ describe('useQuery', () => { createRenderEffect( () => ({ status: state.status, - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, }), () => { - snapshots.push(state.data ? snapshot(state.data) : undefined) - if (state.data) { + snapshots.push(snapshot(state.data)) + if (state.isSuccess) { itemRefs.push({ item0: state.data[0], item1: state.data[1] }) } }, @@ -1046,7 +1051,7 @@ describe('useQuery', () => { return (
- data: {String(state.data?.[1]?.done)} + data: {String(state.data[1]?.done)}
) } @@ -1150,7 +1155,7 @@ describe('useQuery', () => { createRenderEffect( () => ({ status: state.status, - data: state.data, + dataUpdatedAt: state.dataUpdatedAt, isFetching: state.isFetching, isRefetching: state.isRefetching, isSuccess: state.isSuccess, @@ -1235,7 +1240,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1284,7 +1289,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1330,7 +1335,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1387,7 +1392,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1457,7 +1462,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1540,7 +1545,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1613,7 +1618,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1690,7 +1695,7 @@ describe('useQuery', () => { staleTime: 100, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), (s) => { states1.push(s) }, @@ -1705,7 +1710,7 @@ describe('useQuery', () => { staleTime: 10, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), (s) => { states2.push(s) }, @@ -1786,7 +1791,7 @@ describe('useQuery', () => { staleTime: 50, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -1820,7 +1825,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2060,7 +2065,7 @@ describe('useQuery', () => { return (
-

{state.data ?? 'default'}

+

{state.isPending ? 'default' : state.data}

) } @@ -2092,7 +2097,7 @@ describe('useQuery', () => { refetchOnWindowFocus: false, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2128,7 +2133,7 @@ describe('useQuery', () => { refetchOnWindowFocus: () => false, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2164,7 +2169,7 @@ describe('useQuery', () => { refetchOnWindowFocus: true, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2200,7 +2205,7 @@ describe('useQuery', () => { refetchOnWindowFocus: 'always', })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2241,7 +2246,7 @@ describe('useQuery', () => { refetchOnWindowFocus: (query) => (query.state.data || 0) < 1, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2303,7 +2308,7 @@ describe('useQuery', () => { staleTime: Infinity, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2350,7 +2355,7 @@ describe('useQuery', () => { staleTime: 0, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2836,14 +2841,14 @@ describe('useQuery', () => { staleTime: 50, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, ) return (
-
data: {state.data ?? 'null'}
+
data: {state.data}
isFetching: {state.isFetching}
isStale: {state.isStale}
@@ -2891,7 +2896,7 @@ describe('useQuery', () => { initialData: 'initial', })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2933,7 +2938,7 @@ describe('useQuery', () => { initialData: 'initial', })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -2977,7 +2982,7 @@ describe('useQuery', () => { initialDataUpdatedAt: oneSecondAgo, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -3024,7 +3029,7 @@ describe('useQuery', () => { initialDataUpdatedAt: 0, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -3067,7 +3072,7 @@ describe('useQuery', () => { reconcile: false, })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -3340,7 +3345,7 @@ describe('useQuery', () => { queryFn: () => sleep(10).then(() => 'data'), })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -3387,7 +3392,7 @@ describe('useQuery', () => { queryFn: () => sleep(10).then(() => 'data'), })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -3462,7 +3467,7 @@ describe('useQuery', () => { function Page() { const state = useQuery(() => ({ queryKey: key, queryFn })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4121,7 +4126,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4204,7 +4209,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4335,7 +4340,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4457,7 +4462,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4512,7 +4517,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4622,7 +4627,7 @@ describe('useQuery', () => { createRenderEffect( () => state.data, (data) => { - if (data) { + if (state.isSuccess) { dataRefs.push(data) } }, @@ -4680,9 +4685,7 @@ describe('useQuery', () => { })) createTrackedEffect(() => { - if (state.data) { - states.push(state.data) - } + states.push(state.data) }) const forceUpdate = () => { @@ -4834,7 +4837,7 @@ describe('useQuery', () => { const state = useQuery(() => ({ queryKey: [key, id()], queryFn })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4886,7 +4889,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4897,7 +4900,7 @@ describe('useQuery', () => { -
data: {state.data ?? 'null'}
+
data: {state.data}
isFetching: {state.isFetching}
) @@ -4966,7 +4969,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -4980,7 +4983,7 @@ describe('useQuery', () => { -
data: {state.data ?? 'null'}
+
data: {state.data}
) } @@ -4991,7 +4994,7 @@ describe('useQuery', () => { )) - expect(rendered.getByText('data: null')).toBeInTheDocument() + expect(rendered.getByText('data:')).toBeInTheDocument() fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await vi.advanceTimersByTimeAsync(10) @@ -4999,7 +5002,7 @@ describe('useQuery', () => { fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await vi.advanceTimersByTimeAsync(10) - expect(rendered.getByText('data: null')).toBeInTheDocument() + expect(rendered.getByText('data:')).toBeInTheDocument() expect(states.length).toBe(4) @@ -5269,7 +5272,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, @@ -6146,7 +6149,7 @@ describe('useQuery', () => { })) createRenderEffect( - () => ({ ...state }), + () => deep(state), () => { states.push(snapshot(state) as any) }, diff --git a/packages/solid-query/src/types.ts b/packages/solid-query/src/types.ts index 1c5bd10a24..a1ddfabe8a 100644 --- a/packages/solid-query/src/types.ts +++ b/packages/solid-query/src/types.ts @@ -66,10 +66,26 @@ export type UseQueryOptions< /* --- Create Query and Create Base Query Types --- */ +/** + * Reading `data` on a useQuery/useInfiniteQuery result is backed by an async + * resource: while the query is loading, the component is suspended into the + * nearest `` boundary, so by the time `data` is actually read during + * render the value has settled. The type reflects that — `data` is `TData`, + * never `undefined`. + * + * Distributes over the result union so each status variant keeps its other + * discriminants (`status`, `error`, ...) and only `data` is narrowed. + */ +export type NonNullableData = TResult extends { + data: unknown +} + ? Omit & { data: TData } + : never + export type UseBaseQueryResult< TData = unknown, TError = DefaultError, -> = QueryObserverResult +> = NonNullableData, TData> export type UseQueryResult< TData = unknown, @@ -132,7 +148,7 @@ export type UseInfiniteQueryOptions< export type UseInfiniteQueryResult< TData = unknown, TError = DefaultError, -> = InfiniteQueryObserverResult +> = NonNullableData, TData> export type DefinedUseInfiniteQueryResult< TData = unknown, diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 94e3775f4e..1b8d68b58b 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -3,14 +3,17 @@ // why that happens. import { notifyManager, shouldThrowError } from '@tanstack/query-core' import { + NotReadyError, createRenderEffect, createSignal, createStore, + getObserver, isPending, onCleanup, reconcile, refresh, runWithOwner, + sharedConfig, snapshot, untrack, useContext, @@ -500,6 +503,39 @@ export function useBaseQuery< throw state.error } + // `data` is typed non-nullable, so a read that happens before the first + // fetch settles has no value to return. Suspend instead: throwing + // NotReadyError from a tracking scope sends the reader to the nearest + // boundary, mirroring the isServer branch above so both sides + // behave the same. The `state` reads here are what re-subscribe the + // reader, so it re-runs once the subscriber syncs the settled result. + // + // Only `isLoading` suspends (pending *and* fetching). A query that is + // pending but idle — disabled, or reset with no observer fetching — has + // nothing in flight to wait for, so it yields undefined rather than + // parking the boundary on a promise that never resolves. + // + // Untracked reads pass through: event handlers and effect callbacks + // peek at the raw value, which keeps imperative access working (and + // lets callers observe pending states) without suspending. + // + // Hydration stands down: while Solid is claiming server-rendered DOM + // (`sharedConfig.hydrating`), a throw here bails the claim — the + // server rendered this content from settled data that the streaming + // hydration channel may not have primed on the client yet — leaving + // unclaimed server nodes and crashing the reactive system with + // "Potential Infinite Loop Detected". Reads during that window + // return the store value, and the per-query hydration coordinator + // (see hydrationChannel.ts) re-syncs them once their entry lands. + if ( + prop === 'data' && + getObserver() && + state.isLoading && + !sharedConfig.hydrating + ) { + throw new NotReadyError(observer.getCurrentQuery()) + } + return Reflect.get(target, prop, receiver) }, }) diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index a17455cbf8..89e1398dfc 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -11,7 +11,7 @@ import { } from 'solid-js' import { useQueryClient } from './QueryClientProvider' import { useIsRestoring } from './isRestoring' -import type { QueryOptions, UseQueryResult } from './types' +import type { QueryOptions } from './types' import type { Accessor } from 'solid-js' import type { QueryClient } from './QueryClient' import type { @@ -26,6 +26,14 @@ import type { ThrowOnError, } from '@tanstack/query-core' +// Unlike useQuery, useQueries results are a plain reactive store with no +// resource backing — reads never suspend, so `data` here stays nullable +// rather than using the package-wide NonNullableData result type. +type UseQueryResult< + TData = unknown, + TError = DefaultError, +> = QueryObserverResult + // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function does not have a parameter type UseQueryOptionsForUseQueries<