Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

useBaseQuery fetches the old query key when an <Activity> tree is revealed

Đang mở
#12,227 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@lazerg đang làm issue này rồi.

Từ ngày 21/10/2026.

  • #12230 của @lazerg — đang mở
  • #12283 của @vkpdeveloper — đang mở

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
35/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Đình trệ
Công nghệ
react, typescript
Lĩnh vực
frontend

Hướng nghiên cứu

Start in packages/react-query/src/useBaseQuery.ts and compare its subscription with the later observer.setOptions effect; inspect the corresponding useQueries flow and the referenced queryObserver.ts listener check. Reproduce with the Vitest and Testing Library example in the issue, then verify revealing the hidden tree fetches only the current key. An open linked pull request (#12230) is already addressing this issue.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Describe the bug

If a query's key changes while its component is inside <Activity mode="hidden">, revealing the tree fetches the old key once before it switches to the new one.

useBaseQuery hands the changed options to its observer in a passive effect that is declared after the store subscription (packages/react-query/src/useBaseQuery.ts, same on main):

React.useSyncExternalStore(
  React.useCallback(
    (onStoreChange) => {
      const unsubscribe = shouldSubscribe
        ? observer.subscribe(notifyManager.batchCalls(onStoreChange))
        : noop
      observer.updateResult()
      return unsubscribe
    },
    [observer, shouldSubscribe],
  ),
  () => observer.getCurrentResult(),
  () => observer.getCurrentResult(),
)

React.useEffect(() => {
  observer.setOptions(defaultedOptions)
}, [defaultedOptions, observer])

While the tree is hidden, React re-renders it, so defaultedOptions already holds the new key, but it runs no effects. On reveal, effects mount in declaration order:

  1. The subscription runs first. observer.subscribe → onSubscribe → shouldFetchOnMount reads the observer's previous query, which is stale by now, and fetches it.
  2. Only then does setOptions move the observer to the new key, which is fetched as well.

useQueries has the same order: observer.setQueries runs in a useEffect after the subscription.

This is the query side of #11171, where <Activity> re-subscription left a useMutation stuck. That one was fixed in MutationObserver.onSubscribe (#11172, #11217).

Your minimal, reproducible example

A Vitest + Testing Library case (jsdom), no external link:

// @vitest-environment jsdom
import { Activity, Suspense } from 'react'
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '@tanstack/react-query'
import { act, render, screen } from '@testing-library/react'
import { expect, it } from 'vitest'

const fetched: string[] = []

function List({ filter }: { filter: string }) {
  const { data } = useSuspenseQuery({
    queryKey: ['list', filter],
    queryFn: async () => {
      fetched.push(filter)
      return filter
    },
  })
  return <p>{data}</p>
}

it('does not fetch the old key when an Activity is revealed', async () => {
  const client = new QueryClient()
  const view = (mode: 'visible' | 'hidden', filter: string) => (
    <QueryClientProvider client={client}>
      <Activity mode={mode}>
        <Suspense fallback="loading">
          <List filter={filter} />
        </Suspense>
      </Activity>
    </QueryClientProvider>
  )

  const { rerender } = render(view('visible', 'a'))
  await screen.findByText('a')

  // Change the key while hidden: the hidden tree re-renders with 'b', but no effect runs.
  rerender(view('hidden', 'b'))
  // Let both queries go stale (suspense uses a 1s minimum staleTime).
  await act(() => new Promise((resolve) => setTimeout(resolve, 1100)))

  rerender(view('visible', 'b'))
  await screen.findByText('b')
  await act(() => new Promise((resolve) => setTimeout(resolve, 50)))

  // 'a' was fetched once, on the first render. Revealing the tree should not fetch it again.
  expect(fetched.filter((key) => key === 'a')).toEqual(['a'])
})

On 5.101.2 this fails with expected [ 'a', 'a' ] to deeply equal [ 'a' ].

Steps to reproduce
  1. Render a component that calls useSuspenseQuery (or useQuery) inside <Activity mode="visible">.
  2. Switch the Activity to hidden, and change the query key in the same update.
  3. Wait past staleTime (1s under suspense).
  4. Switch the Activity back to visible.
  5. The queryFn runs for the old key, then for the new key.
Expected behavior

Revealing the tree should fetch only the current key. The old key is no longer rendered, and no observer should start a fetch for it.

In our app, the old key named a group the user had just deleted on another tab. Every return to the tab sent requests for that group's records, and they came back 404.

How often does this bug happen?

Every time

Platform
  • OS: macOS
  • Browser: Chromium (also reproduced in jsdom, above)
Tanstack Query adapter

react-query

TanStack Query version

5.101.2 (the code is unchanged on main)

TypeScript version

5.9.3

Additional context
  • React / React DOM 19.2.3, Next.js 16.3.8 (App Router).

  • A fix that works for us: sync the options in a layout effect. Layout effects run before passive effects, including when an <Activity> tree is revealed. setOptions does not fetch while the observer has no listeners (queryObserver.ts, const mounted = this.hasListeners()). So the passive subscription then starts on the current query:

    React.useLayoutEffect(() => {
      observer.setOptions(defaultedOptions)
    }, [defaultedOptions, observer])
    

    We apply the same change to observer.setQueries in useQueries. With that patch on 5.101.2, the case above passes, and so do the suites of our two React apps that use TanStack Query (about 6,000 tests).

  • Another option would be to update the observer's query inside the subscribe callback, before observer.subscribe, with the options from the latest render.

Ngôn ngữ chính
TypeScript
Star
50.4k
Fork
4.3k
Merge trung bình
9 giờ 18 phút
Pull request đã merge (30 ngày)
599

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của TanStack/query

Tất cả issue của TanStack/query

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.