Skip to content

fix(ratewise): 修復 persisted v2 冷載觸發 React #418 hydration mismatch - #664

Merged
s123104 merged 1 commit into
experiment/ratewise-product-2026h2from
fix/653-v2-hydration
Jul 7, 2026
Merged

fix(ratewise): 修復 persisted v2 冷載觸發 React #418 hydration mismatch#664
s123104 merged 1 commit into
experiment/ratewise-product-2026h2from
fix/653-v2-hydration

Conversation

@s123104

@s123104 s123104 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Closes #653(refs #666:Settings 頁 getConverterV2Variant 同構破口由 PM 另立 issue 追蹤,本 PR 不擴 scope)

根因

persisted singleConverterVariant: v2(zustand ratewise-converter)使 SingleConverteruseSyncExternalStore client snapshot 在 hydration 期間即回傳 v2。converterStore rehydrate/__migrateFromLegacy 遷移在 hydration 窗口內觸發 store 更新時,React 會對尚未完成 hydration 的邊界強制改走 client render——此時 client 內容為 v2,與 SSG snapshot(一律 legacy)不符 → React #418(間歇、舊 profile 因遷移路徑必現)。

Two-pass render 設計

  • 第一 pass(hydration)hydrated=false 時,uSES 的 client snapshot 一律沿用 getConverterV2ServerSnapshot(legacy)。hydration 期間任何強制 re-render 的輸出都與 SSG HTML 一致,結構性杜絕 chore(seo): 每日更新匯差範例數據 #418 破口。
  • 第二 pass(commit 後、paint 前)useLayoutEffect(isomorphic,對齊 RememberedHomeRouteLanguagePreferenceSync 同族慣例)設 hydrated=true,uSES 改讀 persisted 偏好切 v2。時序精確化:base 的 uSES 一致性切換由 passive effect 於 paint 後觸發,本修法 layout effect 於 paint 前——切換更早,無多餘可見 legacy 幀。
  • SPA 導覽 remount:模組級旗標記錄本次 page load 已完成 hydration,後續 remount 首幀直接依偏好渲染,不重演 two-pass(零閃爍)。
  • converter-v2-flag.ts SSOT 未修改;flag off 零暴露行為不變(v2 chunk 不載入)。

單元測試宣稱範圍與鑑別力(審查 R1 修正)

jsdom 不可重演 hydration 錯誤的證據(結構+實測):React 的 uSES 在 hydration render 一律讀取 getServerSnapshot(修法版/壞版共用常數 legacy),且 store 訂閱在 commit 後才建立——hydration 窗口內的 store 更新(本測試已於 hydrateRoot 同一 act 窗口內模擬 legacy→v2 set)不構成 React 可見事件。實測壞版 A(base 還原)/壞版 C(hydrated 初值 true)在 hydration 測試下 onRecoverableError 均收 0 筆,無法以 hydration 錯誤鑑別。runtime mismatch 防回歸由 e2e converter-v2.spec.ts「設 v2 冷啟動重載」(寫入 persisted v2 → reload → console error 斷言為空)承擔。

壞版鑑別(實證必紅):新增「client-only 首幀契約」測試——真正的破口在 client render 路徑(hydration de-opt/早期更新 #423/SSG fallback guard 清 root 後重渲染),該路徑首次 render 直讀 client snapshot。以 callback ref 於首次 commit 的 layout 階段(layout effect 切換 re-render 之前)同步取樣 DOM:

變體 結果
修法版 10/10 綠(首幀取樣 legacy
壞版 A(還原 base:無 hydrated gate) 紅:expected [ 'v2' ] to deeply equal [ 'legacy' ]
壞版 C(hydrated 初值 true) 紅:expected [ 'v2' ] to deeply equal [ 'legacy' ]

hydration 錯誤收集同步改用 hydrateRootonRecoverableError callback(正確 API 鉤點;jsdom+React 19 的 mismatch 不走 console.error,原 console spy 為空斷言已移除)。

驗收證據

項目 結果
persisted v2 冷載 console 零 #418 Playwright(chromium)先設 localStorage → reload → 收 console:persisted-v2old-profile-v2(wave-A legacy key 觸發遷移)各 6 輪、含 CPU 6x+Fast 3G 節流與 SW 受控重載情境,console error 全程為空
SSG 輸出不變 base vs fix 兩份 build 共 255 頁 HTML,正規化 build 噪音(asset hash/CSP hash/build 時間/SSG hash)後 0 diffrenderToString 單元測試斷言 persisted v2 下 SSG 輸出仍為 legacy
v2 切換無可察覺閃爍 390×844 冷載 120ms 連拍(screenshots/653-cold-load-frame-*.png):SSG 靜態 legacy HTML → v2 骨架 → v2,幀序與 base 分支對照組完全一致(frame 0 的 legacy 為 SSG 靜態 HTML 本身,hydration 前即存在,非本修法引入;paint 前切換不新增 legacy 幀)
單元測試 vitest run 全套 184 檔 4859 passed(rebase 後含 #663
E2E converter-v2.spec.ts 16 passed(rebase 後 dist 重建重跑)
Hooks pre-commit 6 步(002 記分守門對 staged 全量驗證通過,另手動 node scripts/verify-002-log.mjs 通過)與 pre-push(typecheck+test+build:ratewise)全過

變更檔案

  • apps/ratewise/src/features/ratewise/components/SingleConverter.tsx:two-pass gate(唯一行為變更點)
  • apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.flag.test.tsx:hydration 契約+client-only 首幀契約測試
  • .changeset/persisted-v2-hydration-418.md:patch
  • docs/dev/002_development_reward_penalty_log.md:+1 → 累計 +178(rebase 後依 base +177 重算)

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…e 653)

- SingleConverter 改 two-pass render:hydration commit 前 client snapshot 一律沿用
  server snapshot(legacy),與 SSG 輸出一致,杜絕 rehydrate/遷移期間強制 client render 的 418
- commit 後以 layout effect(paint 前)切換讀取 persisted 偏好;base 的 uSES 一致性切換為
  passive effect paint 後,本修法切換更早且無多餘可見 legacy 幀;SPA remount 首幀直接依偏好渲染
- converter-v2-flag.ts SSOT 未動,flag off 零暴露行為不變
- 契約測試(審查 R1 強化):hydration 錯誤改以 hydrateRoot onRecoverableError 收集;
  新增 client-only 首幀契約——以 callback ref 於首次 commit layout 階段取樣,
  base 還原版與 hydrated 初值 true 壞版均實證必紅,修法版綠
- jsdom 無法以 hydration 錯誤鑑別壞版(uSES hydration render 恆讀 getServerSnapshot、
  訂閱 commit 後才建立),runtime mismatch 防回歸由 converter-v2 e2e 冷載 console 斷言承擔

測試:vitest 全套 4859 passed;converter-v2 e2e 16 passed;SSG 255 頁正規化零 diff;
Playwright persisted v2 冷載(含舊 profile 遷移、CPU/網路節流、SW 受控)console 零 418

Co-authored-by: Cursor <cursoragent@cursor.com>
@s123104
s123104 force-pushed the fix/653-v2-hydration branch from b22254e to 7c0c992 Compare July 7, 2026 00:56
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

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.

1 participant