Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

`createQuery`/`createQueries`: a `queryFn` that reads reactive state before its first `await` causes an infinite observer teardown/refetch loop

未关闭
#11,541 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
78/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
typescript

调研方向

从 packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts 开始,并为 createQuery 添加或运行提供的回归测试。阅读 createBaseQuery 的订阅 effect,并保留被跟踪的 observer 读取,同时阻止 observer.subscribe 被纳入响应式跟踪。完成标准是只发生一次 fetch、状态成功、数据成功写入,并且全部 217 个测试通过。

由索引模型根据 Issue 内容生成。

描述

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 (onSubscribeshouldFetchOnMount#executeFetchQuery.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:

  1. the queryFn reads reactive state before its first await (auth-token store, config store, health store, …);
  2. 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 queryFn to consume ctx.signal, which real transports do: Query.removeObserver only performs a real cancel when abortSignalConsumed, otherwise the fetch is allowed to finish and the loop self-heals;
  3. 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/svelte-query@6.2.1 + svelte@5.57.0:

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

  1. Add the test above to packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts on main and run pnpm test:lib.
  2. Observed on current main: fetches.length is 7 (and keeps growing with more simulated time), query.status stays pending, 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

主要语言
TypeScript
星标
50.3k
派生
4.2k
平均合并
22 小时 33 分钟
30 天内合并 PR
214

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

TanStack/query 的其他 Issue

查看 TanStack/query 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。