Skip to content

fix(queries): restart a running stream on refetch, like a regular query - #206

Merged
mbret merged 2 commits into
fix/live-query-cache-latest-valuefrom
fix/live-query-refetch-restarts-stream
Oct 21, 2026
Merged

mbret merged 2 commits into
fix/live-query-cache-latest-valuefrom
fix/live-query-refetch-restarts-stream

Conversation

@mbret

@mbret mbret commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #205: please merge that one first. I'll then retarget this PR to main and bring it up to date.

Summary

refetch(), queryClient.refetchQueries(), queryClient.invalidateQueries() and queryClient.resetQueries() now restart a running live stream, the way a regular query runs its queryFn again:

  • The fetch in progress is cancelled, which stops the running stream. Then the queryFn runs again and a new stream starts.
  • The data stays until the new stream emits. The exception is resetQueries(), which first puts the query back in its initial state.
  • The refetch settles with the new stream's first value. That happens right away for a stream that emits its current value when subscribed (a BehaviorSubject, most live database queries). For a stream that only emits on events, it happens at the next event.
  • Before the stream's first value, refetch(), refetchQueries() and invalidateQueries() join the fetch in progress instead, because TanStack only cancels a fetch for a refetch when the query has data. The stream keeps running, and they settle with its first value. resetQueries() still restarts it.
  • Automatic refetches still join the running stream, as they join any fetch in progress: window focus, reconnection, another component mounting, refetchInterval. So does refetch({ cancelRefetch: false }).

Before, these calls took over the running stream and settled with its current value. The queryFn never ran again, and resetQueries() did nothing to a live query.

This also fixes a bug with the fetch that follows the stream when it waits for the network (networkMode: "online" while offline). A refetch or cancelQueries() during that wait now stops the stream. Before, the stream kept running after cancelQueries(). After a refetch, the refetch's fetch ended up following the old stream once back online, so awaiting the refetch never settled.

What changes for consumers

  • An explicit refetch or invalidation of a live query subscribes to its observable again. A cold observable, such as a socket or a polling request, reconnects, because its queryFn runs again.
  • Awaiting a refetch or invalidation of a stream that only emits on events now waits for its next event, the way a regular query's refetch waits for its queryFn. This includes a mutation whose onSuccess returns invalidateQueries(): the mutation stays pending until that event.
  • resetQueries() now puts a live query back in its initial state and restarts its stream.

Migration:

  • To keep the data fresh without restarting the stream, don't refetch at all: the stream already keeps the data up to date. If something must call refetch, use refetch({ cancelRefetch: false }): it joins the running stream and settles when the stream ends.
  • The docs' "cancel, then refetch" recipe for restarting a stream is no longer needed: refetch() restarts it.
  • If a mutation shouldn't wait for an event-only stream, call invalidateQueries() in onSuccess without returning or awaiting it.

Implementation (createObservableQueryFn)

  • A fetch TanStack starts runs the queryFn again. Only the fetch the bridge starts to follow the stream joins the running stream. The take-over branch is gone.
  • Cancelling a fetch now stops its stream immediately. Before, the stop waited a microtask so that a refetch in the same tick could take the stream over.
  • The fetch that follows the stream can wait for the network before it runs, and has no abort signal of ours yet. When TanStack cancels it during that wait, its rejection now stops the stream.
  • The next guard for a stream that was cancelled but kept is removed, because no such stream exists any more.

Tests

  • liveCache:
    • "restarts the stream and settles with its first value" replaces the take-over test. It checks that the queryFn runs again, the old stream is unsubscribed, the data stays until the new stream emits, and the awaited refetch settles at the new stream's first value.
    • New: "restarts the stream from an empty query on resetQueries".
    • New: "joins the stream on a refetch before its first value".
    • "holds a value as soon as the stream emits it while a fetch settles" now checks a value emitted right after the new stream's first one.
    • The cancel-then-refetch test is removed, because refetch() alone restarts the stream now.
  • efficiency:
    • Invalidation now makes a second subscription: { made: 2, open: 1 }.
    • New: "does not render for an invalidation that brings the same value".
  • useQueries$: invalidation restarts both streams.
  • offline:
    • A refetch while the follow fetch waits for the network restarts the stream once back online.
    • cancelQueries during that wait stops the stream.

Each new or changed test that pins the restart fails against #205's bridge. The two offline tests also fail without the network-wait handling.

Restarting doesn't change render counts. An invalidation that brings the same value causes 0 renders for a component that reads data, and 1 for one that also reads isFetching, as before.

Checks

  • npm run check and npm run build pass.
  • npx vitest run --typecheck: 255 tests pass, no type errors.
  • npm run test:ci: 3 runs on the latest commit, all green.
  • The changed test files: 10 runs, all green. useQuery$.invalidation.test.tsx (StrictMode): 12 runs, all green.

Release

Like #199, #204 and #205, this is a fix: commit with no BREAKING CHANGE: footer. It does reverse documented behaviour: the docs said refetching kept a running stream. If you want a major release, add a BREAKING CHANGE: footer when squashing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH

`refetch()`, `refetchQueries()`, `invalidateQueries()` and
`resetQueries()` took a running stream over and settled with its current
value: the `queryFn` never ran again, and `resetQueries()` left the
query as it was. A regular query cancels the fetch in progress and runs
its `queryFn` again.

Live queries now do the same: the running stream stops, a new one
starts, and the refetch settles with its first value, right away for a
stream that emits its current value when subscribed. Automatic refetches
(window focus, reconnection, mount, `refetchInterval`) still join the
running stream, as they join any fetch in progress.

A refetch or `cancelQueries()` while the fetch that follows the stream
waits for the network now stops the stream too. It kept running before,
and the refetch's fetch ended up following it, never settling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T13:24:24.995364Z 5284c16 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5284c162e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread gitbook/reactjrx/queries/forever-query.md
TanStack cancels the fetch in progress on a refetch only when the query
has data. Before a live query's first value, `refetch()`,
`refetchQueries()` and `invalidateQueries()` therefore join the fetch in
progress: the stream keeps running, and they settle with its first
value. `resetQueries()` still restarts it.

"Forever Query" said every explicit refetch restarts the stream. It now
states the exception, and a test pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH
@mbret
mbret merged commit fd8aeec into fix/live-query-cache-latest-value Oct 21, 2026
2 checks passed
mbret added a commit that referenced this pull request Oct 21, 2026
* fix(queries): leave a stopped live query's freshness to staleTime

When a live query's stream stopped (its last observer unmounting,
`cancelQueries`, the query getting disabled, a fetch of an unused
query), the query was also marked stale, so the next observer refetched
it whatever its `staleTime`. A regular query keeps its freshness when
its fetch is cancelled: `staleTime` alone decides when it refetches.

Live queries now do the same. With the default `staleTime`, which the
docs recommend for them, nothing changes: the value is stale as soon as
the stream stops, so the next observer that needs it reopens the
stream. With a longer one, the cached value is shown without a stream
until it goes stale, as the docs already warn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH

* fix(queries): restart a running stream on refetch, like a regular query (#206)

* fix(queries): restart a running stream on refetch, like a regular query

`refetch()`, `refetchQueries()`, `invalidateQueries()` and
`resetQueries()` took a running stream over and settled with its current
value: the `queryFn` never ran again, and `resetQueries()` left the
query as it was. A regular query cancels the fetch in progress and runs
its `queryFn` again.

Live queries now do the same: the running stream stops, a new one
starts, and the refetch settles with its first value, right away for a
stream that emits its current value when subscribed. Automatic refetches
(window focus, reconnection, mount, `refetchInterval`) still join the
running stream, as they join any fetch in progress.

A refetch or `cancelQueries()` while the fetch that follows the stream
waits for the network now stops the stream too. It kept running before,
and the refetch's fetch ended up following it, never settling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH

* docs(queries): a refetch before a stream's first value joins its fetch

TanStack cancels the fetch in progress on a refetch only when the query
has data. Before a live query's first value, `refetch()`,
`refetchQueries()` and `invalidateQueries()` therefore join the fetch in
progress: the stream keeps running, and they settle with its first
value. `resetQueries()` still restarts it.

"Forever Query" said every explicit refetch restarts the stream. It now
states the exception, and a test pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLxad4gLrsk68haFCJrakH

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants