Skip to content

fix(ratewise): persisted 欄位 hydration 暴露面盤點與 Settings 同構破口修復(#666) - #692

Merged
s123104 merged 1 commit into
experiment/ratewise-product-2026h2from
fix/666-hydration-inventory
Jul 8, 2026
Merged

fix(ratewise): persisted 欄位 hydration 暴露面盤點與 Settings 同構破口修復(#666)#692
s123104 merged 1 commit into
experiment/ratewise-product-2026h2from
fix/666-hydration-inventory

Conversation

@s123104

@s123104 s123104 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closes #666

摘要

依 issue #666 完成三件事:

  1. 全 persisted 欄位 hydration 暴露面盤點(下表):13 個 ratewise-converter partialize 欄位+5 個獨立 localStorage key × 預渲染頁 × server snapshot × 破口判定。
  2. Settings.tsx 同構破口修復:套用 fix(ratewise): 修復 persisted v2 冷載觸發 React #418 hydration mismatch #664 同款 two-pass gate(/settings 為預渲染頁,APP_ONLY_PRERENDER_PATHS)。
  3. 盤點揭露的系統性缺陷修復converterStoreonRehydrateStorage 後置回呼經模組綁定 useConverterStore.getState() 呼叫——localStorage 為同步 storage,回呼於 create() 內同步觸發,此刻模組綁定仍在 TDZ,ReferenceError 被 persist middleware 靜默吞掉:wave-A 遷移與 sanitize 於冷載全滅、persist.hasHydrated() 恆 false(真瀏覽器 production build 實證:wave-A key 冷載後偏好未併入、legacy key 未刪)。改用回呼參數 state(閉包 get/set)修復。

盤點表(欄位 × 預渲染頁消費點 × server snapshot × 破口判定)

判定基準:React uSES 於 hydration render 恆讀 getServerSnapshot(zustand hook 的 server snapshot=getInitialState=預設值),故 hydration render 天然安全;破口類別為 client render 路徑(hydration de-opt/早期更新 #423/SSG fallback guard)首幀直讀 client snapshot,輸出偏離 SSG HTML 即 #418 同族。

zustand ratewise-converter(partialize 13 欄)

欄位 預渲染頁消費點 server snapshot 破口判定
lastConverterView /(RememberedHomeRoute redirect) 'single' 豁免:既有 hydrated gate——hydrate 完成前不 Navigate,首幀恆 single=SSG
singleConverterVariant /(SingleConverter 分流);/settings(aria-pressed+說明文字+override badge) 'legacy' / 已修(#664 gate);/settings 破口 → 本 PR 修
fromCurrency /(converter props) 'TWD' 豁免(殘餘同族面附註 ①)
toCurrency /(converter props) 'JPY' 豁免(附註 ①)
rateMode /(換算數值);/settings(aria-pressed+說明文字) 'auto' /settings 破口 → 本 PR 修/ 見附註 ①
rateType /(RateSelector pills+數值) 'spot' 豁免(附註 ①)
rateSource /(來源 badge+數值) 'bank' 豁免(附註 ①)
providerPreference /(effectiveRateSource 推導) manual/bank 豁免(附註 ①)
favorites /(FavoritesList,md+);/favorites 預設 6 幣 /favorites 豁免(isHydrated skeleton gate:SSG 與 client 首幀同為 skeleton);/ 見附註 ①
history /favorites [] 豁免(isHydrated skeleton gate)
baseCurrency /multi(multiAmounts initializer 讀 getState() 'TWD' 豁免(/multi isHydrated skeleton gate;首頁不渲染 multi 元素)
cardRateEnabled /(uSES → hasCardRate pill) false 豁免:uSES 已配 getCardRateServerSnapshot;Phase 1 無 UI 寫入路徑(僅 URL override,不持久化),persisted true 僅能手改 localStorage;殘餘面同附註 ①
cardFeePercent /(CardEstimateInfo,僅 hasCardRate=true 渲染) 1.5 豁免:受 cardRateEnabled 零暴露 gate 連動,SSG 恆不渲染 card UI

獨立 localStorage key(/settings 消費)

Key/欄位 預渲染頁消費點 server snapshot 破口判定
ratewise-theme.style /settings(7 風格卡 aria-pressed/ring) 'zen' 破口 → 本 PR 修(React DOM 面;<html data-style> 由 index.html pre-paint bootstrap 處理,非 hydration 比對範圍)
ratewise-theme.customPrimary /settings(自訂卡 ring/裝飾圓/勾選色) #3182F6 破口 → 本 PR 修
ratewise-theme.customBackgroundTone /settings(僅 CustomThemeSheet 開啟後消費) 'pure' 豁免:首幀 sheet 恆 closed(draft=null),無 DOM 暴露
ratewise-splash-enabled /settings(switch aria-checked) true 破口 → 本 PR 修
ratewise-language 全站文字 'zh-TW'(i18n 固定 init) 豁免:LanguagePreferenceSync 於 hydration 後 effect 才 changeLanguage,首幀恆 zh-TW=SSG

附註 ①(首頁 zustand hook 殘餘面,誠實標注):hydration render 由 uSES server snapshot 契約涵蓋(恆讀預設值);de-opt 需異步 set() 恰落 hydration suspend 窗口。首頁 route element 非 lazy(無長 suspend 窗口包裹 store 消費點),暴露面顯著小於 lazy route 的 /settings;修復 TDZ 後遷移 set() 發生於模組載入(hydrateRoot 之前),不再構成窗口內事件。此殘餘面由 e2e console 零錯誤矩陣(含 wave-A 遷移組合)持續守門,未在本 PR 擴大 gate 手術(對齊 #664「不擴 scope」裁決)。

內容頁(faq/about/guide/幣別頁 34+金額頁):不消費任何 persisted 欄位(CurrencyLandingPage 無 converterStore/useAppTheme import)——整類豁免。

統計:盤點 18 欄位;破口 6(Settings 5 個消費點+1 個 TDZ 系統性缺陷);修復 6;豁免 12(均附理由)。

修復點清單

  1. apps/ratewise/src/pages/Settings.tsx:two-pass gate(比照 fix(ratewise): 修復 persisted v2 冷載觸發 React #418 hydration mismatch #664 慣例)——
    • 第一 pass(hydration):variantrateModestylecustomPrimarysplash 顯示值一律用 SSG 預設;override badge 的 uSES client snapshot 在 hydrated=false 時固定走 server snapshot。
    • 第二 pass:isomorphic layout effect 於 paint 前切回 persisted 偏好(無多餘可見預設幀)。
    • SPA 導覽 remount:模組級旗標記錄本次 page load 已完成 hydration,首幀直接依偏好渲染(零閃爍)。
  2. apps/ratewise/src/stores/converterStore.tsonRehydrateStorage 回呼改用參數 state(閉包 get/set),修復 TDZ 死路——wave-A 遷移、legacy key 清理、sanitize、hasHydrated() 冷載恢復生效。

合約測試清單(壞版必紅實證)

測試 檔案 壞版鑑別
SSG server snapshot:persisted 非預設值不得影響預渲染輸出 Settings.first-frame.test.tsx
client-only 首幀契約:首次 commit 恆 SSG 預設(variant/rateMode/style/customPrimary/splash 五欄全取樣)、layout effect 後呈現偏好 同上 壞版(hydrated 初值 true)3 紅(實測)
URL override badge 首幀不得出現 同上 同上突變紅
SPA remount 首幀直接呈現偏好(不重演 two-pass) 同上
hydration onRecoverableError 零錯誤+窗口內 store 更新(#653 情境) 同上 同上突變紅
模組首次載入即完成 hydration(hasHydrated=true converterStore.rehydrate.test.ts 壞版(還原模組綁定寫法)3 紅(實測)
wave-A legacy key 冷載自動遷移併入+刪除舊 key 同上 同上
損毀 persisted 欄位冷載自動 sanitize 同上 同上

宣稱範圍(誠實標注,同 #664):jsdom 中 React hydration render 對 uSES 恆讀 server snapshot、store 訂閱 commit 後才建立,壞版無法以 hydration 錯誤重演;壞版鑑別由 client-only 首幀取樣承擔,runtime mismatch 防回歸由 e2e 承擔。

E2E(3 組高風險 persisted 組合冷載,chromium desktop+mobile)

組合 內容 結果
1 singleConverterVariant=v2rateMode=sell(issue 核心破口) console 零錯誤+UI 呈現偏好 ✅
2 ratewise-theme style=nitro+customPrimary console 零錯誤+主題卡選中 ✅
3 全欄位疊加+wave-A legacy key 遷移(#653 情境) console 零錯誤+遷移生效 ✅(base 分支此組必紅:TDZ 死路使 v2 偏好遺失,aria-pressed 恆 false——修復前後對照實證)

驗證

  • pnpm --filter @app/ratewise exec vitest run195 檔 9328 passed(3 skipped)
  • settings-hydration.spec.tsconverter-v2.spec.ts34 passed(desktop+mobile)
  • pre-commit 6 步/pre-push(typecheck+test+build:ratewise)全過
  • SSG /settings 輸出契約:legacy/zen/auto/splash-on 全為預設、無 override badge(build 後實測)
  • changeset:patch;002:+1 → 累計 +202

Made with Cursor

- Settings 套用 issue 664 同款 two-pass gate:variant、rateMode、theme、customPrimary、
  splash 首幀恆用 SSG 預設,layout effect 於 paint 前切回 persisted 偏好,remount 不重演
- 修復 converterStore onRehydrateStorage TDZ 死路:同步 storage 下後置回呼於 create()
  內觸發時模組綁定仍在 TDZ,改用回呼參數 state 走閉包,wave-A 遷移與 sanitize 恢復生效
- 新增 Settings 首幀契約測試(client-only 首幀取樣,壞版突變必紅)與 converterStore
  rehydrate 契約測試(模組綁定壞版必紅)
- 新增 e2e settings-hydration.spec.ts:三組高風險 persisted 組合冷載 console 零錯誤

測試:vitest run 全綠(9331)、settings-hydration 與 converter-v2 e2e 34 passed、typecheck 過

Co-authored-by: Cursor <cursoragent@cursor.com>
@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.

@github-actions

github-actions Bot commented Jul 8, 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 8, 2026

Copy link
Copy Markdown
Contributor

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

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

@s123104
s123104 merged commit eac973d into experiment/ratewise-product-2026h2 Jul 8, 2026
11 checks passed
@s123104

s123104 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

PM 收棒補註(審查 4 條文件級 Should-fix 歸檔):1) 盤點表漏列 december-theme 獨立 key——併入 #666 關閉留言補記;2) 補救結論如實記錄:legacy key 未刪、wave-A 偏好於修復後冷載可自動找回(遷移重新生效);3) /multi /favorites 豁免依據為本地旗標非 store hasHydrated,自洽成立;4) TDZ 存在於 production main 約 4 個月(2026-03-08 起)——main hotfix 決策已另立 P1 issue 待使用者裁決。

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