Skip to content

fix: derive server useId values from tree position - #102

Closed
BleedingDev wants to merge 2 commits into
TanStack:mainfrom
BleedingDev:fix/ssr-useid-tree-position
Closed

BleedingDev wants to merge 2 commits into
TanStack:mainfrom
BleedingDev:fix/ssr-useid-tree-position

Conversation

@BleedingDev

@BleedingDev BleedingDev commented Oct 28, 2026 •

Copy link
Copy Markdown

Why the change

Suspended server renders must produce the same useId values as hydration, without colliding with IDs allocated by later client mounts.

Special things to note

Change outline

Reproduction with identifierPrefix: 'app-', where Slow suspends until after the shell has flushed:

function Field() { return <span id={useId()}>field</span> }
function Slow() { pending.read(); return <Field /> }
<main><Suspense fallback={null}><Slow /></Suspense><Field /></main>

IDs in DOM order after reveal:

- Server:    app-1, app-0
- Hydration: app-0, app-1
+ Both:      _app-R_1_, _app-R_2_

A separate prefix collision also occurs on main: the first server/hydrated field and a new client-only field can both receive app-0; the new namespaces use _app-R_0_ and _app-r_0_ instead.

On main, packages/redact/src/server/dispatcher.ts:129-131 uses a shared render-order counter, and packages/redact/src/dom/dispatcher.ts:464-472 lets an explicit prefix erase the server/client distinction.

 server walk
-  shared counter follows render completion order
+  fork bigint tree positions, including empty and iterable child slots
+  reset hook-local counters at each component
+  retain tree positions across Suspense retries
+  reuse the leading-bit helper used by client tree IDs
 client root
-  tree IDs only with identifierStrategy: 'react'
+  tree IDs by default; separate R_ hydration and r_ client namespaces

Added regressions in tests/ssr-tree-ids.test.tsx:

hydrates tree-position IDs after boundaries resolve out of order (prefix %s) [2 cases]
separates explicit-prefix hydrated IDs from new client IDs and other roots
matches nested arrays, empty slots, iterables and useId parents with deep descendants

Existing literal-ID expectations change without removing DOM-identity assertions; a patch changeset accompanies the runtime change.

Node 24.22.0
pnpm test       185 files passed, 5 skipped; 2,005 tests passed, 91 skipped
pnpm test:types passed
pnpm build      passed, including 550 explicit imports and 112 NodeNext declarations
pnpm size:check 38/38 passed
React compatibility size comparison against 0c7274e passed, all 36 rows

server gzip      main     this branch    delta    old cap    new cap
source           8,975 B  9,179 B         +204 B  8,989 B    9,179 B
built            8,974 B  9,177 B         +203 B  8,989 B    9,177 B

Sharing the leading-bit helper saves 2 source and 4 built gzip bytes compared with the original independent patch. The compatibility comparison permits only measured positive deltas against the exact base, including server min/gzip/Brotli growth and at most 48 bytes of client Brotli variation; other entries and metrics retain zero growth.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved consistency of server-rendered IDs across nested content and streamed Suspense boundaries, including when boundaries resolve out of order.
    • Preserved IDs on hydrated content while keeping IDs unique for content mounted later on the client.
    • Improved matching of server- and client-generated IDs with optional identifier prefixes and across deeply nested content, arrays, and iterables.
    • Updated hydration behavior to recognize the revised ID format and avoid recoverable hydration errors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 319d709

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/redact Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 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 ⚠️ Failed 2026-10-28T14:03:26.424028Z
We couldn’t complete this review. Comment @codex review to try again.
353841d 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.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 468f750b-fcb7-4167-8a92-b0dbb0c1fad4
Commits

Reviewing files that changed from the base of the PR and between 353841d and 319d709.

Files selected for processing: 1
  • tests/ssr-tree-ids.test.tsx

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Walkthrough

Server rendering now derives useId values from React tree positions. The walker carries tree IDs through child traversal and suspended-boundary re-renders. Tests cover hydration behavior, and size-check scripts add SSR-specific allowances.

Changes: SSR useId tree-position identifiers

Layer / File(s) Summary
SSR frame and ID format
packages/redact/src/dom/react-ids.ts, packages/redact/src/dom/root-internal.ts, packages/redact/src/server/dispatcher.ts
The SSR frame now tracks tree and local IDs. useId formats identifiers from those values, and roots always set ri to true.
Tree-slot traversal and suspended re-renders
packages/redact/src/server/walk.ts, packages/redact/src/server/stream.ts
The walker assigns tree IDs to child positions, sets and restores frame IDs during component rendering, and passes boundary tree IDs into re-renders.
Hydration validation and SSR size allowances
tests/ssr-tree-ids.test.tsx, tests/hydration.test.tsx, tests/mixed-renderer-hooks.test.tsx, scripts/compatibility-size-budget.mjs, scripts/size-check.mjs, tests/compatibility-size-budget.test.ts, .changeset/ssr-useid-tree-position.md
Tests cover tree-position IDs across hydration and streaming cases. Size-check scripts add SSR-specific allowances, and a patch changeset records the update.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Walk
  participant Dispatcher
  participant StreamBoundary
  Walk->>Dispatcher: Set treeId and localId for component rendering
  Dispatcher-->>Walk: Return useId value from treeId and localId
  Walk->>StreamBoundary: Store treeId on suspended boundary
  StreamBoundary->>Walk: Re-render boundary children with treeId
Loading

Possibly related PRs

  • TanStack/redact#76: Changes client React ID assignment to use logical child slots, which PR #102 applies to SSR tree positions.
  • TanStack/redact#95: Introduces the compatibility size-allowance mechanism that PR #102 extends for SSR.

Suggested reviewers: tannerlinsley

Merge Risk: Minimal · up to 319d7

No concrete merge-blocking risk was found in the changed SSR ID and hydration paths.

Pre-merge checks: Passed 4 · Failed 1

Failed 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%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "fix: derive server useId values from tree position" matches the main change. The PR replaces the server render-order counter in the SSR dispatcher and walker with bigint tree positions, so …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "fix: derive server useId values from tree position" matches the main change. The PR replaces the server render-order counter in the SSR dispatcher and walker with bigint tree positions, so server useId values follow React-style tree IDs. The title is short, specific, and states the primary change from the developer's view.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 7 functions across 11 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
⚔️ Resolve merge conflicts: 💡
  • Resolve merge conflict in branch fix/ssr-useid-tree-position
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Copy link
Copy Markdown

Adding independent browser confirmation of the released bug and this proposed fix. This currently blocks our streamed-SSR adoption evaluation.

To rule out application/configuration mistakes, we used a standalone complete HTML document with renderToReadableStream, native bootstrapModules, and hydrateRoot(document, sameTree). The HTTP response forwards the renderer's stream unchanged. Suspension uses documented use(stablePromise), with promises created outside render. This test imports the renderer's public entries directly and contains no Start, Router, Base UI, application code or Vite aliases.

The tree has two suspended fields (A and B) separated by a synchronous parent/field. Fields use useId() for their input/label/help associations; the parent also uses an ID. We resolve B before A, waiting for B's emitted segment before releasing A, then wait for both fields in the intended main element before hydration. Server and client both use identifierPrefix: 'app-'.

Runtime Streamed ID result
React 19.3.1 No hydration errors; server nodes and IDs retained.
Published Redact 0.2.2 Two recoverable Hydration mismatch. errors; A/B field nodes replaced and their IDs changed, while the synchronous middle field survives.
Source-built base 0c7274e Reproduces the released failure.
This PR at 353841dfeceed3b91fac7eedd7eccc14ea69731b Same fixture passes; no hydration errors, captured nodes/IDs retained.

The ready-ID control and a plain streamed-content control pass on both released runtimes and this PR. Separately, our unchanged generic Vite fixtures pass both default-prefix and explicit-prefix ID cases with this PR, after failing on the base.

This supports attribution to Redact's runtime rather than our app or stream wrapper. It does not establish a surviving duplicate-ID defect: after recovery, the released runtime's final label controls work. We have not qualified a complete application, performance, or a combined/released version of these fixes.

Environment: Linux, Bun 1.4.4, React/ReactDOM 19.3.1, Playwright 1.64.1, Chromium 156.0.8078.4, NODE_ENV=production. The published 0.2.2 tarball was integrity-checked; the source-built base and PR variants used the same build toolchain.

Compare every hydrated descendant by reference so identical replacement markup cannot satisfy the adoption regression.

Co-Authored-By: Claude Code <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