useId values depend on render order, so a lazy component loaded only on the server makes hydration discard server HTML (0.3.1)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 54/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- react, typescript
- 領域
- frontend
調査の方向性
Start by reading the ID counters in server/dispatcher.ts and dom/dispatcher.ts, then trace how the server and browser assign IDs when a lazy component suspends only in the browser. Reproduce the issue with the provided field.jsx, app.jsx, server.jsx, and client.jsx example by calling render() twice before hydration. Done means the second server render hydrates without replacing the section or either label.
索引モデルが issue の本文から書いたものです。
説明
Summary
Redact's useId values depend on render order, so they change when a lazy component is already loaded on the server but not yet in the browser. The ID mismatch makes Redact silently discard the server-rendered section and render it again on the client. React keeps all of its server HTML in the same app.
This is the normal case on a running server: the lazy module loads on the first request and renders inline from then on, while each new visitor's browser still has to fetch it.
Reproduction
field.jsx
import { useId } from 'react'
export default function LazyField() {
const id = useId()
return <label id={id}>lazy</label>
}
app.jsx
import { Suspense, lazy, useId } from 'react'
const LazyField = lazy(() => import('./field.jsx'))
function Field() {
const id = useId()
return <label id={id}>eager</label>
}
export function App() {
return (
<main>
<Suspense fallback={<p>loading section</p>}>
<section>
<Suspense fallback={<p>loading field</p>}>
<LazyField />
</Suspense>
<Field />
</section>
</Suspense>
</main>
)
}
server.jsx
import { renderToReadableStream } from 'react-dom/server'
import { App } from './app.jsx'
export async function render() {
const stream = await renderToReadableStream(<App />)
await stream.allReady
return new Response(stream).text()
}
client.jsx
import { hydrateRoot } from 'react-dom/client'
import { App } from './app.jsx'
hydrateRoot(document.getElementById('root'), <App />)
Build the server and client with and without the redact() plugin. Call render() twice and serve the second result, as a server would after its first request. Then load the client bundle.
| Runtime | Server HTML | After hydration | Server nodes | Errors reported |
|---|---|---|---|---|
| React 19.3.1 | _R_1_, _R_2_ |
_R_1_, _R_2_ |
All kept | None |
| Redact 0.3.1 | :R0, :R1 |
:r1, :r0 |
section and both labels replaced |
None |
I checked node identity with a snapshot taken in an inline script before the client bundle loads. The result was the same across repeated runs.
The first render() doesn't show the problem. There the lazy component suspends on the server too, so the numbers happen to line up.
Cause
From reading the source, both sides use a counter in render order: frame.idCounter++ in server/dispatcher.ts and root.ic++ in dom/dispatcher.ts. In this case:
- The warm server renders
LazyFieldfirst, so it gets:R0andFieldgets:R1. - In the browser,
LazyFieldsuspends and its boundary is skipped, soFieldtakes:R0. :R0doesn't match the server's:R1, so hydration of the outer boundary fails and it recovers by rendering on the client.
React avoids this by deriving IDs from tree position. Redact already has that logic on the client for native React servers (identifierStrategy: 'react'), so using tree-position IDs on Redact's own server may be one way to fix it. I haven't tested that.
Real-world impact
In a TanStack Start app with code-split components, the top section of a page was rebuilt about 150 ms after load for signed-in visitors, which throws away the work the server already did. An experimental patch that corrected the mismatched attribute in place stopped the rebuild, which points to the ID mismatch as the trigger.
Because nothing is reported, this is easy to miss. No error is logged in a production build unless the app has passed its own onRecoverableError, and I didn't test that path.
Environment
@tanstack/redact0.3.1, defaultredact()preset- React 19.3.1 (control)
- Vite 7.3.7,
@vitejs/plugin-react5.2.0 - Chromium 156 (Playwright), Node 26
- 主要言語
- TypeScript
- スター
- 537
- フォーク
- 22
- 平均マージ
- 10時間 27分
- マージ済み PR(30日)
- 40
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/redact のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
TanStack/redact#142 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 55/100
TanStack/redact#164 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 49/100
メンテナーはふだん 1 日以内に返信
TanStack/redact の issue をすべて見る
似ている issue
-
bug needs-triage potential-regression
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
aws/aws-sdk-js-v3#8408 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
colinhacks/zod#6743 ·
メンテナーはふだん 1 日以内に返信
-
[BUG] Packing list: the weight field drops digits on a slow connection and saves the wrong weightオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 79/100
openai/openai-node#3124 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
anthropics/anthropic-sdk-typescript#1296 ·
メンテナーはふだん 8 日以内に返信