diff --git a/.changeset/warm-caches-notify.md b/.changeset/warm-caches-notify.md
new file mode 100644
index 00000000000..4e6cc9afd95
--- /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 f85e85a3cd0..ad47ba45c5f 100644
--- a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
+++ b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx
@@ -6,7 +6,14 @@
// 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,
+ createMemo,
+ createSignal,
+} from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryCache, QueryClient, useQuery } from '..'
import { renderWithClient } from './utils'
@@ -509,5 +516,107 @@ 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', async () => {
+ const key = queryKey()
+ const queryFn = () => 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)} />}
+ >
+
+
+ )
+ }
+
+ const rendered = renderWithClient(queryClient, () => (
+ loading}>
+
+
+ ))
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(rendered.getByText('cache: 1')).toBeInTheDocument()
+
+ fireEvent.click(rendered.getByRole('button', { name: 'mount' }))
+ await vi.advanceTimersByTimeAsync(10)
+
+ expect(rendered.getByText('query: 1')).toBeInTheDocument()
+ expect(rendered.getByText('projection: 1')).toBeInTheDocument()
+ })
+
+ // Solid #3181 fixed this projection/memo notification path in 2.0.0-rc.5.
+ // Keep it active here so the Query read layer cannot reintroduce #11351.
+ it('notifies a leaf reader that goes through a memo over data', async () => {
+ const key = queryKey()
+ const server = { flag: false }
+ const observed: Array = []
+
+ function Page() {
+ const state = useQuery(() => ({
+ queryKey: key,
+ queryFn: () => sleep(10).then(() => ({ ...server })),
+ }))
+ const data = createMemo(() => state.data)
+ createEffect(
+ () => data().flag,
+ (flag) => {
+ observed.push(flag)
+ },
+ )
+ return flag: {String(state.data.flag)}
+ }
+
+ const rendered = renderWithClient(queryClient, () => (
+ loading}>
+
+
+ ))
+
+ await vi.advanceTimersByTimeAsync(10)
+ expect(observed).toEqual([false])
+
+ server.flag = true
+ void queryClient.refetchQueries({ queryKey: key })
+ await vi.advanceTimersByTimeAsync(10)
+
+ expect(rendered.getByText('flag: true')).toBeInTheDocument()
+ expect(observed.at(-1)).toBe(true)
+ })
})
})
diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts
index d152a685349..4fd011843aa 100644
--- a/packages/solid-query/src/useBaseQuery.ts
+++ b/packages/solid-query/src/useBaseQuery.ts
@@ -212,6 +212,18 @@ export function useBaseQueryLayer<
let observerSub: (() => void) | null = null
let cacheSub: (() => void) | null = null
let disposed = false
+ let pulledQueryBeforeAttach: Query<
+ TQueryFnData,
+ TError,
+ TQueryData,
+ TQueryKey
+ > | null = null
+ /**
+ * 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
@@ -348,7 +360,29 @@ export function useBaseQueryLayer<
createRenderEffect(
() => isRestoring(),
(restoring) => {
- if (!restoring) attach()
+ if (!restoring) {
+ const currentQuery = observer.getCurrentQuery()
+ if (pulledQueryBeforeAttach === currentQuery) {
+ attach()
+ return
+ }
+ const state = currentQuery.state
+ const optimisticFetchStatus = observer.getOptimisticResult(
+ untrack(defaultedOptions),
+ ).fetchStatus
+ if (
+ state.data !== undefined &&
+ optimisticFetchStatus !== state.fetchStatus
+ ) {
+ deferredMountFetchStatus = optimisticFetchStatus
+ queueMicrotask(() => {
+ deferredMountFetchStatus = null
+ attach()
+ })
+ } else {
+ attach()
+ }
+ }
},
)
}
@@ -529,6 +563,7 @@ export function useBaseQueryLayer<
* sees the identical options object — a no-op diff.
*/
if (!isServer) observer.setOptions(opts as any)
+ if (!observerSub) pulledQueryBeforeAttach = q
return chainOnce(q.fetch(opts as any), select, wrap)
}
return NEVER
@@ -599,12 +634,19 @@ 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 metaProjection = createProjection(
- (draft) => {
- Object.assign(draft, metaFrom(query().state))
- },
- untrack(() => metaFrom(query().state)),
- )
+ const projectedMeta = () => {
+ const state = query().state
+ if (deferredMountFetchStatus !== null && state.data !== undefined) {
+ return {
+ ...metaFrom(state),
+ fetchStatus: deferredMountFetchStatus,
+ }
+ }
+ return metaFrom(state)
+ }
+ const metaProjection = createProjection((draft) => {
+ Object.assign(draft, projectedMeta())
+ }, untrack(projectedMeta))
const meta = isServer
? new Proxy({} as MetaState, {
get: (_, key) => serverMeta()[key as keyof MetaState],