Skip to content

fix(hydration): set dataUpdatedAt when pending query resolves before hydration#10610

Open
DORI2001 wants to merge 3 commits intoTanStack:mainfrom
DORI2001:fix/hydration-dateupdatedat-streamed-query
Open

fix(hydration): set dataUpdatedAt when pending query resolves before hydration#10610
DORI2001 wants to merge 3 commits intoTanStack:mainfrom
DORI2001:fix/hydration-dateupdatedat-streamed-query

Conversation

@DORI2001
Copy link
Copy Markdown

@DORI2001 DORI2001 commented Apr 29, 2026

Problem

When a query is dehydrated while still pending (streamed) but then resolves before hydration runs on the client, the hydration code transitions the query's status from 'pending' to 'success' — but it never updates dataUpdatedAt. It stays at 0.

This was introduced in #10444, which removed the query.fetch() call that previously set dataUpdatedAt as a side effect of resolving the promise.

Steps to reproduce: dehydrate a pending query (e.g. from a React Server Component), resolve it before sending the response, then hydrate on the client. query.state.dataUpdatedAt will be 0 instead of a real timestamp.

Fix

When the pending→success transition is applied (both in the setState path for existing queries and in the queryCache.build path for new queries), add dataUpdatedAt: dehydratedAt ?? Date.now().

dehydratedAt is the right value here — it records when the server serialized the resolved data, which is when the data was actually "updated". Falling back to Date.now() handles older dehydration payloads that don't include dehydratedAt.

Fixes #10603

Summary by CodeRabbit

  • Bug Fixes

    • Ensure restored queries that resolved while pending receive an updated data timestamp so freshness is tracked correctly.
  • Tests

    • Added hydration tests verifying resolved pending queries (both new and existing cache entries) get a valid data timestamp after restore.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

📝 Walkthrough

Walkthrough

When hydrating queries that were pending at dehydration but already have resolved data, the hydration path now sets state.dataUpdatedAt to dehydratedAt if present, otherwise to Date.now(). This update is applied for both restoring new queries and updating existing ones.

Changes

Hydration timestamp correction

Layer / File(s) Summary
Core Fix
packages/query-core/src/hydration.ts
When state.status === 'pending' && data !== undefined, set state.dataUpdatedAt to dehydratedAt if available, else Date.now() during hydration.
Restore vs Update Paths
packages/query-core/src/hydration.ts
Apply the dataUpdatedAt logic in both query.setState (existing query) and queryCache.build / restore (new query) flows.
Tests
packages/query-core/src/__tests__/hydration.test.tsx
Add two tests: hydrate resolved streamed/promise queries into (1) an empty cache entry and (2) an existing stale entry; assert status === 'success', correct data, and state.dataUpdatedAt > 0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A pending query woke with data so bright,
Its timestamp restored in the soft morning light.
From dehydrated time or the clock ticking now,
No zeros remain — hydration takes a bow. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: setting dataUpdatedAt when a pending query resolves before hydration.
Description check ✅ Passed The PR description provides a clear problem statement, detailed fix explanation, and rationale. Both required checklist sections are present in the template.
Linked Issues check ✅ Passed The code changes fully address issue #10603 requirements: dataUpdatedAt is set to dehydratedAt or Date.now() when pending queries resolve before hydration in both setState and build paths.
Out of Scope Changes check ✅ Passed All changes are scoped to hydration.ts and hydration.test.tsx files, directly addressing the fix for dataUpdatedAt in pending-to-success query transitions without unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/query-core/src/hydration.ts (1)

233-257: ⚠️ Potential issue | 🟠 Major

Older payloads can still skip pending→success upgrade in existing-query hydration.

When dehydratedAt is missing (older payload), hasNewerSyncData is always false (Line 237), and state.dataUpdatedAt is typically 0 for pending dehydrated state. That can keep the if on Line 239 false, so the transition at Line 253 and the timestamp fix at Line 256 never run for existing pending queries.

Suggested fix
-        const hasNewerSyncData =
-          syncData &&
-          // We only need this undefined check to handle older dehydration
-          // payloads that might not have dehydratedAt
-          dehydratedAt !== undefined &&
-          dehydratedAt > query.state.dataUpdatedAt
+        const hasNewerSyncData =
+          syncData &&
+          (dehydratedAt !== undefined
+            ? dehydratedAt > query.state.dataUpdatedAt
+            : // Older payloads: allow only pending->success upgrade for existing pending-without-data
+              state.status === 'pending' &&
+              query.state.status === 'pending' &&
+              query.state.data === undefined)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/hydration.ts` around lines 233 - 257, The hydration
logic currently skips the pending→success upgrade when dehydratedAt is missing;
update the hasNewerSyncData computation to treat an undefined dehydratedAt as
“newer” for pending dehydrated states (e.g., set hasNewerSyncData = syncData &&
(dehydratedAt !== undefined ? dehydratedAt > query.state.dataUpdatedAt :
state.status === 'pending')). Then ensure when you call query.setState (same
block around query.setState and the pending→success branch) you still use
dataUpdatedAt: dehydratedAt ?? Date.now() so the timestamp correction runs even
if dehydratedAt was missing. This references the variables dehydratedAt,
syncData, state.status, state.dataUpdatedAt and the call to query.setState.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/query-core/src/hydration.ts`:
- Around line 233-257: The hydration logic currently skips the pending→success
upgrade when dehydratedAt is missing; update the hasNewerSyncData computation to
treat an undefined dehydratedAt as “newer” for pending dehydrated states (e.g.,
set hasNewerSyncData = syncData && (dehydratedAt !== undefined ? dehydratedAt >
query.state.dataUpdatedAt : state.status === 'pending')). Then ensure when you
call query.setState (same block around query.setState and the pending→success
branch) you still use dataUpdatedAt: dehydratedAt ?? Date.now() so the timestamp
correction runs even if dehydratedAt was missing. This references the variables
dehydratedAt, syncData, state.status, state.dataUpdatedAt and the call to
query.setState.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16c83652-7389-4dc8-9f6d-243d04125a88

📥 Commits

Reviewing files that changed from the base of the PR and between c5ab5a1 and 5a4c8db.

📒 Files selected for processing (1)
  • packages/query-core/src/hydration.ts

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented May 3, 2026

please add a test case that covers this scenario - something that fails on main but works on your PR.

@DORI2001
Copy link
Copy Markdown
Author

DORI2001 commented May 5, 2026

Added two test cases to hydration.test.tsx that verify dataUpdatedAt > 0 after hydrating a pre-resolved streamed query — one for a fresh cache entry (the queryCache.build path) and one for an existing entry (the query.setState path). Both fail on main and pass with this PR.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/query-core/src/__tests__/hydration.test.tsx (1)

1808-1848: 💤 Low value

LGTM — new cache-entry test correctly validates the fix.

The synchronous-thenable setup faithfully models a React streaming promise that resolved before hydrate(), and toBeGreaterThan(0) will correctly fail on main (where dataUpdatedAt stays 0) and pass with the fix applied.

One minor asymmetry with Test 2: adding a pre-hydration dataUpdatedAt === 0 assertion would make it explicit that the value was 0 before hydration and becomes > 0 after — mirroring the explicit before/after pattern in the existing-cache-entry test.

🔍 Optional symmetry improvement
   const clientQueryClient = new QueryClient()
+  const query = clientQueryClient.getQueryCache().find({ queryKey: key })
   hydrate(clientQueryClient, dehydrated)
 
-  const query = clientQueryClient.getQueryCache().find({ queryKey: key })!
+  // query is only created during hydrate, so the pre-hydration reference
+  // must be obtained lazily; alternatively assert post-hoc, as below.

Or, more simply, assert dehydrated.queries[0]?.state.dataUpdatedAt equals 0 right after dehydrate(), paralleling what Test 2 does with expect(query.state.dataUpdatedAt).toBe(0) at line 1882.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-core/src/__tests__/hydration.test.tsx` around lines 1808 -
1848, Add an explicit pre-hydration assertion that the dehydrated query's
dataUpdatedAt is 0: after calling dehydrate(serverQueryClient) assert
dehydrated.queries[0]?.state.dataUpdatedAt === 0 (or toBe(0)) before calling
hydrate(clientQueryClient, dehydrated) so the test mirrors the
existing-cache-entry test and demonstrates the value changes to > 0 after
hydrate; locate the check around the existing references to dehydrate(),
dehydrated, and hydrate() in the test "should set dataUpdatedAt when hydrating a
resolved streamed query into a new cache entry".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/query-core/src/__tests__/hydration.test.tsx`:
- Around line 1808-1848: Add an explicit pre-hydration assertion that the
dehydrated query's dataUpdatedAt is 0: after calling
dehydrate(serverQueryClient) assert dehydrated.queries[0]?.state.dataUpdatedAt
=== 0 (or toBe(0)) before calling hydrate(clientQueryClient, dehydrated) so the
test mirrors the existing-cache-entry test and demonstrates the value changes to
> 0 after hydrate; locate the check around the existing references to
dehydrate(), dehydrated, and hydrate() in the test "should set dataUpdatedAt
when hydrating a resolved streamed query into a new cache entry".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e93e9ba7-bd3b-4820-b86b-1ee978a7dc5e

📥 Commits

Reviewing files that changed from the base of the PR and between 5a4c8db and 177a0a5.

📒 Files selected for processing (1)
  • packages/query-core/src/__tests__/hydration.test.tsx

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dataUpdatedAt incorrect for streamed queries that resolve before hydration

2 participants