`createQuery`/`createQueries`: a `queryFn` that reads reactive state before its first `await` causes an infinite observer teardown/refetch loop
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 78/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- typescript
- Área
- frontend, testing-qa
Línea de trabajo
Comienza con packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts y añade o ejecuta la prueba de regresión proporcionada para createQuery. Lee el efecto de suscripción de createBaseQuery y conserva la lectura rastreada del observer, mientras proteges observer.subscribe del seguimiento reactivo. Se considera terminado cuando haya un fetch, un estado correcto, los datos se hayan cargado y las 217 pruebas pasen.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Describe the bug
A queryFn that reads any reactive state ($state) synchronously — before its first await — silently adds that state to the dependencies of the internal subscription $effect in createQuery/createQueries. Every subsequent write to that state re-runs the effect: the observer is torn down (cancelling any in-flight fetch) and re-subscribed. If the fetch takes longer than the interval between writes, the query never receives data, and since it stays dataless every re-subscription starts a new fetch via shouldFetchOnMount — an unbounded fetch loop.
The root cause: the subscription effect calls observer.subscribe(...). For a query without data, query-core executes queryFn synchronously inside that call (onSubscribe → shouldFetchOnMount → #executeFetch → Query.fetch → retryer → queryFn), i.e. inside the effect's tracking scope. Reactive reads performed in that window (everything before the queryFn's first await) are recorded as dependencies of the effect.
The full loop requires all of the following, each of which is ordinary app code:
- the
queryFnreads reactive state before its firstawait(auth-token store, config store, health store, …); - that state is written while a fetch is in flight (e.g. the fetch's own error/backoff handling marks the store). The resulting teardown cancels the in-flight fetch — this needs the
queryFnto consumectx.signal, which real transports do:Query.removeObserveronly performs a real cancel whenabortSignalConsumed, otherwise the fetch is allowed to finish and the loop self-heals; - the fetch is slower than the write cadence.
Context / impact
This affects createQuery, createInfiniteQuery (both via createBaseQuery) and createQueries. We hit this in a production app: a queryFn reading coordinator health stores before a signed RPC (~300 ms) drove a sustained loop of ~3.2 cancelled-and-retried signed requests per second, indefinitely — each cancellation having already burned the expensive work. Workaround for apps: wrap reactive reads inside queryFn in untrack() — but since the library executes queryFn inside its own effect, the library should shield that execution.
Your minimal, reproducible example
ts
Steps to reproduce
Self-contained regression test (also included in the companion PR), run against @tanstack/[email protected] + [email protected]:
it(
'should not re-subscribe when queryFn reads reactive state before its first await',
withEffectRoot(async () => {
const key = queryKey()
const tick = ref(0)
const fetches: Array<number> = []
const query = createQuery<number, Error>(
() => ({
queryKey: key,
queryFn: async (ctx) => {
void ctx.signal // consume the abort signal, like real transports do
const startedAt = tick.value // reactive read before the first await
fetches.push(startedAt)
await sleep(150)
tick.value = startedAt + 1 // write mid-flight (e.g. a health mark)
await sleep(150)
return startedAt
},
}),
() => queryClient,
)
await vi.advanceTimersByTimeAsync(1000)
expect(fetches.length).toBe(1)
expect(query.data).toBe(0)
expect(query.status).toBe('success')
}),
)
Steps to reproduce
- Add the test above to
packages/svelte-query/tests/createQuery/createQuery.svelte.test.tsonmainand runpnpm test:lib. - Observed on current
main:fetches.lengthis 7 (and keeps growing with more simulated time),query.statusstayspending, data never lands.
Expected behavior
Exactly one fetch; the write to tick must not re-run the internal subscription effect — the queryFn's reactive reads should not become dependencies of the library's effect.
Suggested fix
Keep the observer read tracked, but run the subscription itself untracked:
$effect(() => {
const o = observer
const unsubscribe = isRestoring.current
? () => undefined
: untrack(() => o.subscribe(() => update(createResult())))
return unsubscribe
})
One subtlety worth noting: the naive untrack(() => observer.subscribe(...)) is wrong — it also untracks the observer read, so changing queries would no longer re-subscribe. The existing test "should track queries added to an initially empty array" catches this, which is why the const o = observer read must stay outside the untrack. With the corrected fix, all 217 tests pass (215 existing + the 2 regression tests from the PR).
Expected behavior
The internal subscription effect must not gain dependencies from the queryFn's execution. Reading reactive state inside a queryFn (before its first await) is ordinary usage and should not re-run the subscription effect — so a write to that state must not tear the observer down or cancel the in-flight fetch. The regression test above should observe exactly one queryFn invocation, query.status === 'success', and the fetched data landing.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
jsdom via vitest, and Chromium (real app)
Tanstack Query adapter
None
TanStack Query version
6.2.1 (latest at time of writing; also verified on 6.1.33)
TypeScript version
6.0.3
Additional context
No response
- Lenguaje dominante
- TypeScript
- Estrellas
- 50.4k
- Forks
- 4.2k
- Merge medio
- 14 h 34 min
- PR fusionados (30 d)
- 332
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de TanStack/query
-
package: angular-query-devtools
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
TanStack/query#11680 · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
TanStack/query#11358 · 2 comentarios · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
TanStack/query#11106 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
setQueryData: NoInfer loses discriminated-union members when spreading a narrowed updater valueAbierto
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
Los mantenedores suelen responder en 1 día
-
Dificultad 4/5 3-5 días Aptitud para principiantes 68/100
TanStack/query#11794 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de TanStack/query
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Doist/todoist-cli#576 ·
Los mantenedores suelen responder en 1 día
-
Suggestion: document (or optionally add) a cheaper-model config for find-skills on Claude CodeAbiertofeature
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
vercel-labs/skills#2370 ·
Los mantenedores suelen responder en 1 día
-
🐛 Bug supabase/cli
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
CopilotKit/aimock#491 ·
Los mantenedores suelen responder en 1 día