From b50b7bdc67c0013befc18410b3b3c08647449c27 Mon Sep 17 00:00:00 2001 From: Mauro Garcia Date: Tue, 1 Sep 2026 10:48:28 -0300 Subject: [PATCH] test(solid-query): pin the stale read through a memo over query.data A tracked computation that reaches a leaf THROUGH a `createMemo(() => query.data)` indirection is not re-notified after a refetch, so its last observation stays the superseded value permanently. A wrapper hook that gates or narrows `query.data` before handing it to consumers is exactly this shape, so it reaches app code that never writes the memo explicitly. The data node is not at fault: the derive runs the expected number of times, the commit lands, the DOM swaps, and an untracked read through the very same memo returns the new value. Every other reader shape is notified correctly, and the behaviour reproduces with no TanStack code involved, so the test is skipped pending the upstream fix in solidjs/solid#3181. Refs #11351 Co-authored-by: Cursor --- .../src/__tests__/useQuery-semantics.test.tsx | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx index f85e85a3cd0..2aa57ee5bf9 100644 --- a/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery-semantics.test.tsx @@ -6,7 +6,13 @@ // 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, + 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 +515,61 @@ describe('useQuery 2.0 read semantics', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('n: 42')).toBeInTheDocument() }) + + // Reproduction for #11351. A tracked computation that reaches a leaf + // THROUGH a `createMemo(() => state.data)` indirection stops being + // notified after a refetch: the memo is an async consumer of the data + // node, its committed value is the same store face on every commit, and + // the propagation is pruned at the memo — so the effect's own reads of + // that same node are never re-evaluated. Its last observation is the + // superseded value, permanently. + // + // This is not a defect in the data node itself: the derive runs the + // expected four times, the commit lands, and an untracked read through + // the very same memo returns the new value. Every other reader shape + // (leaf read in the effect, leaf read inside the memo, leaf read off an + // untracked-captured face, a suspended memo over an UNRELATED async + // source) is notified correctly, and the behaviour reproduces with no + // TanStack code involved: solidjs/solid#3181. + // + // eslint-disable-next-line vitest/no-disabled-tests -- pending the upstream fix, kept as the reproduction + it.skip('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) + + // The direct read swaps, so the commit itself landed. + expect(rendered.getByText('flag: true')).toBeInTheDocument() + // ...but the reader behind the memo was never told. + expect(observed.at(-1)).toBe(true) + }) }) })