diff --git a/.changeset/disabled-query-server-read.md b/.changeset/disabled-query-server-read.md
new file mode 100644
index 0000000000..64b1ca8f2a
--- /dev/null
+++ b/.changeset/disabled-query-server-read.md
@@ -0,0 +1,12 @@
+---
+'@tanstack/solid-query': patch
+---
+
+fix: commit the idle read of a disabled query on the server. A query
+disabled with nothing cached parked its reader on a promise that never
+settles, which is the intended client behavior — an enabling change, a
+refetch or a cache write revives it. The server has no later: the render
+has to finish, and nothing will enable the query or write the cache
+before it does, so the render stalled forever and emitted nothing at all.
+The server now commits the idle read, which is the settled SSR truth for
+a disabled query and the same contract its meta channel already honors.
diff --git a/packages/solid-query/src/__tests__/fixtures/hydration/App.tsx b/packages/solid-query/src/__tests__/fixtures/hydration/App.tsx
index 58fcc6eba9..bab3ea67c8 100644
--- a/packages/solid-query/src/__tests__/fixtures/hydration/App.tsx
+++ b/packages/solid-query/src/__tests__/fixtures/hydration/App.tsx
@@ -21,6 +21,7 @@ export interface FetchCounts {
stale: number
placeholder: number
prefetched: number
+ disabled: number
}
export interface AppProps {
@@ -100,6 +101,29 @@ function Queries(props: AppProps) {
)
}
+/** Reads the data of a query that is disabled with nothing cached. On the
+ * server that read has nothing to wait on and no later in which to get one —
+ * the render has to finish, so the idle read is its settled SSR truth and the
+ * document is emitted. (On the client the same read parks the reader in this
+ * boundary until something starts the query, which is why it lives in a
+ * boundary of its own here: the rest of the app hydrates around it.) */
+function DisabledConsumer(props: AppProps) {
+ const disabled = useQuery(() => ({
+ queryKey: ['disabled'],
+ queryFn: async () => {
+ props.counts.disabled++
+ await sleep(5)
+ return `disabled-${props.source}`
+ },
+ enabled: false,
+ }))
+ return (
+
+ {String(disabled.data)}|{disabled.status}|{String(disabled.isEnabled)}
+
+ )
+}
+
/** Never rendered on the server — mounted by tests after hydration. Its
* query was prefetched (and only prefetched) during SSR; the hash-keyed
* registry entry must satisfy it with zero client fetches. */
@@ -139,6 +163,9 @@ export function App(props: AppProps) {
+ loading}>
+
+
)
}
diff --git a/packages/solid-query/src/__tests__/fixtures/hydration/entry-client.tsx b/packages/solid-query/src/__tests__/fixtures/hydration/entry-client.tsx
index 9a67bea7b7..c07deb6515 100644
--- a/packages/solid-query/src/__tests__/fixtures/hydration/entry-client.tsx
+++ b/packages/solid-query/src/__tests__/fixtures/hydration/entry-client.tsx
@@ -19,6 +19,7 @@ export function createApp() {
stale: 0,
placeholder: 0,
prefetched: 0,
+ disabled: 0,
}
const [lateMount, setLateMount] = createSignal(false)
return {
diff --git a/packages/solid-query/src/__tests__/fixtures/hydration/entry-server.tsx b/packages/solid-query/src/__tests__/fixtures/hydration/entry-server.tsx
index e96396c264..dfe922cecd 100644
--- a/packages/solid-query/src/__tests__/fixtures/hydration/entry-server.tsx
+++ b/packages/solid-query/src/__tests__/fixtures/hydration/entry-server.tsx
@@ -15,6 +15,7 @@ const counts: FetchCounts = {
stale: 0,
placeholder: 0,
prefetched: 0,
+ disabled: 0,
}
// Fully-settled single-string render. Collected through pipe() rather than
diff --git a/packages/solid-query/src/__tests__/hydration-utils.ts b/packages/solid-query/src/__tests__/hydration-utils.ts
index 9b53daad17..a72bd1761a 100644
--- a/packages/solid-query/src/__tests__/hydration-utils.ts
+++ b/packages/solid-query/src/__tests__/hydration-utils.ts
@@ -36,6 +36,7 @@ export interface ServerReport {
stale: number
placeholder: number
prefetched: number
+ disabled: number
}
queries: Array
}
@@ -54,6 +55,7 @@ export interface ClientBundle {
stale: number
placeholder: number
prefetched: number
+ disabled: number
}
showLate: () => void
mount: (container: HTMLElement) => () => void
diff --git a/packages/solid-query/src/__tests__/hydration.test.tsx b/packages/solid-query/src/__tests__/hydration.test.tsx
index f62c04d55d..390cbdc35f 100644
--- a/packages/solid-query/src/__tests__/hydration.test.tsx
+++ b/packages/solid-query/src/__tests__/hydration.test.tsx
@@ -44,6 +44,7 @@ describe('SSR hydration', () => {
stale: 1,
placeholder: 0,
prefetched: 1,
+ disabled: 0,
})
expect(string.html).toContain('fresh-server')
expect(string.html).toContain('stale-server')
@@ -97,6 +98,18 @@ describe('SSR hydration', () => {
expect(ph.replace(//g, '')).toBe(
'placeholder-value|true|success',
)
+
+ // A disabled query with nothing cached has no fetch to wait for, so its
+ // read settles as the idle state (data|status|isEnabled) and the render
+ // completes. Parking it on a never-settling promise instead deadlocks
+ // the whole render — the server has no later in which the query could
+ // become enabled, so this render would emit nothing at all.
+ const disabled = /]*id="disabled"[^>]*>(.*?)<\/span>/.exec(
+ string.html,
+ )![1]!
+ expect(disabled.replace(//g, '')).toBe(
+ 'undefined|pending|false',
+ )
})
it('hydration primes the query cache and refetches only per staleness rules', async () => {
@@ -165,6 +178,14 @@ describe('SSR hydration', () => {
)
})
+ // The disabled query hydrates to the identical idle face the server
+ // serialized, and stays disabled: no priming (it has no data to
+ // transfer) and no fetch, on mount or after the window closes.
+ expect(container.querySelector('#disabled')?.textContent).toBe(
+ 'undefined|pending|false',
+ )
+ expect(app.counts.disabled).toBe(0)
+
// The serialized observer results no longer carry a hydrationData
// copy at all — the node payload is the only transport.
const registry = (globalThis as any)._$HY.r as Record
diff --git a/packages/solid-query/src/types.ts b/packages/solid-query/src/types.ts
index 927af03e54..b2003bb984 100644
--- a/packages/solid-query/src/types.ts
+++ b/packages/solid-query/src/types.ts
@@ -76,6 +76,10 @@ export type UseQueryOptions<
* (committed, placeholder, initial), or throws (`` /
* `throwOnError`). The v5 `TData | undefined` face existed because reads
* could observe the pre-fetch gap; here that gap is suspension.
+ *
+ * The server is the one exception: suspending needs a later, and a render
+ * that has to finish has none for a query nothing will ever enable. A
+ * disabled read commits `undefined` there rather than stalling the render.
*/
export type UseBaseQueryResult<
TData = unknown,
diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts
index d152a68534..5d032edc2d 100644
--- a/packages/solid-query/src/useBaseQuery.ts
+++ b/packages/solid-query/src/useBaseQuery.ts
@@ -531,6 +531,19 @@ export function useBaseQueryLayer<
if (!isServer) observer.setOptions(opts as any)
return chainOnce(q.fetch(opts as any), select, wrap)
}
+ /**
+ * Disabled, so there is nothing to pull and nothing in flight. Parking
+ * the reader (see `NEVER`) is right on the client — an enabling change,
+ * a refetch or a cache write revives it later. The server has no later:
+ * the render must finish, and nothing will enable the query or write
+ * the cache before it does, so parking there stalls the render forever
+ * and emits nothing at all. The idle read IS the settled SSR truth —
+ * the same contract the meta channel already honors ('pending' serves
+ * as-is for a disabled query) — so commit it and let the client hydrate
+ * the identical state. Committed directly rather than through `wrap`:
+ * `select` must not run on an absent value.
+ */
+ if (isServer) return { value: undefined as TData }
return NEVER
}