[query-broadcast-client-experimental] broadcastQueryClient: an `added` event overwrites a receiving query that has no data (e.g. in error) with the sender's empty state · Issue #12213 · TanStack/query · GitHub
Skip to content

[query-broadcast-client-experimental] broadcastQueryClient: an added event overwrites a receiving query that has no data (e.g. in error) with the sender's empty state #12213

Open

Description

@Madoid82

Package: @tanstack/query-broadcast-client-experimental 5.104.3 (with @tanstack/query-core 5.104.3)

Describe the bug

When another tab creates a query, the receiving tab applies the sender's state on added whenever its own query has no data. A query that has no data but did settle (a failed query: status: 'error', errorUpdatedAt > 0) is replaced with the sender's brand-new state: status: 'pending', fetchStatus: 'idle', error: null, dataUpdatedAt: 0, errorUpdatedAt: 0.

The receiving query now looks like it is loading, but no fetch runs. Observers render a pending/loading UI indefinitely (until a refetch interval, focus or invalidation happens to trigger), and the error is lost.

src/index.ts, lines 193-198:

      } else if (type === 'added') {
        if (query) {
          if (query.state.data === undefined) {
            query.setState(state)
          }
          return
        }

The sender posts added straight from queryCache.build, before its fetch starts (lines 155-162), so the state it carries is the initial empty state:

    if (queryEvent.type === 'added') {
      safePost({
        type: 'added',
        queryHash,
        queryKey,
        state,
      })
    }

Minimal reproduction

  1. Tab A and tab B both call broadcastQueryClient({ queryClient }).
  2. In tab A, useQuery({ queryKey: ['x'], queryFn: () => Promise.reject(new Error('down')), retry: false }). It settles in status: 'error', with no data.
  3. Tab B creates the same query: either mount the same useQuery, or queryClient.getQueryCache().build(queryClient, { queryKey: ['x'] }) (for example, as a side effect of a prefetch that has not started yet).
  4. In tab A, the query is now status: 'pending', fetchStatus: 'idle', error: null, errorUpdatedAt: 0. The observer shows loading, and no request is in flight.

Without the plugin, the same can be shown in a single client:

const query = queryClient.getQueryCache().find({ queryKey: ['x'] })! // in error, no data
query.setState({ status: 'pending', fetchStatus: 'idle', data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0 })
// observers now report pending with no fetch running

Expected behavior

An added event carries nothing the receiver lacks when the sender's query has never settled. The receiver should keep its own state, in particular an error.

Suggested fix

Skip an incoming added state that never received data or an error:

      } else if (type === 'added') {
        if (query) {
          const incoming = state as QueryState
          if (
            query.state.data === undefined &&
            (incoming.dataUpdatedAt > 0 || incoming.errorUpdatedAt > 0)
          ) {
            query.setState(incoming)
          }
          return
        }

A stricter variant would apply added only when it is newer than the receiver's last outcome (max(dataUpdatedAt, errorUpdatedAt)).

Workaround we use

Our query projection detects an observed query left pending with fetchStatus: 'idle' and refetches it once through its observer.

Related

Follow-up to #11391, which made added skip a receiver that already has data. A receiver without data but with an error is still overwritten.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions