diff --git a/.changeset/warm-caches-notify.md b/.changeset/warm-caches-notify.md
new file mode 100644
index 0000000000..4e6cc9afd9
--- /dev/null
+++ b/.changeset/warm-caches-notify.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/solid-query': patch
+---
+
+fix: prevent cached query updates from being lost during mount.
diff --git a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
index ae6130f8d9..02a4b1f9b9 100644
--- a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
+++ b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
@@ -6,7 +6,7 @@
// re-pointed separately.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent } from '@solidjs/testing-library'
-import { Errored, Loading, createSignal } from 'solid-js'
+import { Errored, Loading, Show, createEffect, createSignal } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryCache, QueryClient, useQuery } from '..'
import { renderWithClient } from './utils'
@@ -509,6 +509,70 @@ describe('useQuery 2.0 read semantics', () => {
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('n: 42')).toBeInTheDocument()
})
+
+ it('notifies a consumer mounted over stale cached data without a Loading boundary', async () => {
+ const key = queryKey()
+ const queryFn = vi.fn(() =>
+ sleep(10).then(() => [{ text: 'value' }] as const),
+ )
+
+ function WarmCache(props: { mount: () => void }) {
+ const state = useQuery(() => ({ queryKey: key, queryFn }))
+ return (
+ <>
+ cache: {state.data.length}
+
+ >
+ )
+ }
+
+ function Consumer() {
+ const state = useQuery(() => ({ queryKey: key, queryFn }))
+ const [projection, setProjection] = createSignal<
+ ReadonlyArray<{ text: string }>
+ >([])
+
+ createEffect(
+ () => state.data.slice(),
+ (value) => {
+ setProjection(value)
+ },
+ )
+
+ return (
+ <>
+ query: {state.data.length}
+ projection: {projection().length}
+ >
+ )
+ }
+
+ function App() {
+ const [mounted, setMounted] = createSignal(false)
+ return (
+ setMounted(true)} />}
+ >
+
+
+ )
+ }
+
+ // A Loading boundary masks the lost notification on a cached mount.
+ const rendered = renderWithClient(queryClient, () => )
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('cache: 1')).toBeInTheDocument()
+ expect(queryFn).toHaveBeenCalledTimes(1)
+
+ fireEvent.click(rendered.getByRole('button', { name: 'mount' }))
+ await vi.advanceTimersByTimeAsync(10)
+
+ expect(queryFn).toHaveBeenCalledTimes(2)
+ expect(rendered.getByText('query: 1')).toBeInTheDocument()
+ expect(rendered.getByText('projection: 1')).toBeInTheDocument()
+ })
})
describe('cache removal', () => {
diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts
index f469a27ca9..68f13af2c5 100644
--- a/packages/solid-query/src/useBaseQuery.ts
+++ b/packages/solid-query/src/useBaseQuery.ts
@@ -6,6 +6,7 @@ import {
createSignal,
isPending as isValuePending,
onCleanup,
+ onSettled,
resolve,
runWithOwner,
sharedConfig,
@@ -212,6 +213,12 @@ export function useBaseQueryLayer<
let observerSub: (() => void) | null = null
let cacheSub: (() => void) | null = null
let disposed = false
+ /**
+ * A cached mount can start a refetch while a conditional subtree is still
+ * creating its effects. Defer only that attach until the subtree is ready,
+ * while exposing the observer's optimistic fetch status synchronously.
+ */
+ let deferredMountFetchStatus: MetaState['fetchStatus'] | null = null
/** Set once the mount flow decides the observer should be live — a client
* swap re-attaches the rebuilt observer iff its predecessor was attached. */
let shouldAttach = false
@@ -367,10 +374,35 @@ export function useBaseQueryLayer<
primeAndAttach()
}
if (!hydratedMount) {
+ const restoringOnMount = untrack(isRestoring)
createRenderEffect(
() => isRestoring(),
(restoring) => {
- if (!restoring) attach()
+ if (!restoring) {
+ // Restore completion already runs after the subtree is ready.
+ // Keep its attachment in the same commit as the restored read.
+ if (restoringOnMount) {
+ attach()
+ return
+ }
+ const currentQuery = observer.getCurrentQuery()
+ const state = currentQuery.state
+ const optimisticFetchStatus = observer.getOptimisticResult(
+ untrack(defaultedOptions),
+ ).fetchStatus
+ if (
+ state.data !== undefined &&
+ optimisticFetchStatus !== state.fetchStatus
+ ) {
+ deferredMountFetchStatus = optimisticFetchStatus
+ onSettled(() => {
+ deferredMountFetchStatus = null
+ attach()
+ })
+ } else {
+ attach()
+ }
+ }
},
)
}
@@ -662,11 +694,20 @@ export function useBaseQueryLayer<
* the client must have a server counterpart (and vice versa) or every id
* downstream shifts and hydration key-misses the whole subtree.
*/
+ const projectedMeta = (state: QueryState) => {
+ if (deferredMountFetchStatus !== null && state.data !== undefined) {
+ return {
+ ...metaFrom(state),
+ fetchStatus: deferredMountFetchStatus,
+ }
+ }
+ return metaFrom(state)
+ }
const metaProjection = createProjection(
(draft) => {
- Object.assign(draft, metaFrom(query().state))
+ Object.assign(draft, projectedMeta(query().state))
},
- untrack(() => metaFrom(lookupQuery().state)),
+ untrack(() => projectedMeta(lookupQuery().state)),
)
const meta = isServer
? new Proxy({} as MetaState, {