From 6cdf6012134270a04bfb7372c69292960f3f747a Mon Sep 17 00:00:00 2001 From: haotool Date: Sat, 27 Jun 2026 22:19:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(ratewise):=20=E4=BF=AE=E5=BE=A9=E5=86=B7?= =?UTF-8?q?=E5=95=9F=E5=8B=95=20multi=20=E9=82=84=E5=8E=9F=20hydration=20?= =?UTF-8?q?=E6=9C=AA=E8=A7=B8=E7=99=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RememberedHomeRoute 補 queueMicrotask 與 store subscribe fallback - 新增 production MODE 回歸測試(hasHydrated=false 但 store 已同步) - 新增 patch changeset 測試:RememberedHomeRoute + coldStartRestore 11 項通過;pnpm --filter @app/ratewise test 2469 項通過 Co-authored-by: Cursor --- .../cold-start-multi-hydration-fallback.md | 5 +++ .../components/RememberedHomeRoute.tsx | 43 ++++++++++++++++--- .../__tests__/RememberedHomeRoute.test.tsx | 14 ++++++ .../dev/002_development_reward_penalty_log.md | 7 ++- 4 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 .changeset/cold-start-multi-hydration-fallback.md diff --git a/.changeset/cold-start-multi-hydration-fallback.md b/.changeset/cold-start-multi-hydration-fallback.md new file mode 100644 index 000000000..1774dc784 --- /dev/null +++ b/.changeset/cold-start-multi-hydration-fallback.md @@ -0,0 +1,5 @@ +--- +'@app/ratewise': patch +--- + +修復冷啟動時多幣別換算模式無法還原的問題(生產環境 persist 已同步但 hydration 回呼未觸發)。 diff --git a/apps/ratewise/src/features/ratewise/components/RememberedHomeRoute.tsx b/apps/ratewise/src/features/ratewise/components/RememberedHomeRoute.tsx index c8a4f2cce..62bee02e0 100644 --- a/apps/ratewise/src/features/ratewise/components/RememberedHomeRoute.tsx +++ b/apps/ratewise/src/features/ratewise/components/RememberedHomeRoute.tsx @@ -6,6 +6,7 @@ import { markRestoreAttempted, shouldRestoreToMulti, isPersistedMultiPendingStoreSync, + readPersistedLastConverterView, } from './coldStartRestore'; /** @@ -20,14 +21,46 @@ export function RememberedHomeRoute() { const [hydrated, setHydrated] = useState(isTestEnv); useEffect(() => { + let active = true; + const markHydrated = () => { + if (active) { + setHydrated(true); + } + }; + + const isStoreSyncedWithPersist = (): boolean => { + const persisted = readPersistedLastConverterView(); + if (persisted === null) return true; + return useConverterStore.getState().lastConverterView === persisted; + }; + if (useConverterStore.persist.hasHydrated()) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- persist hydration marker - setHydrated(true); - return undefined; + markHydrated(); + return () => { + active = false; + }; } - return useConverterStore.persist.onFinishHydration(() => { - setHydrated(true); + + const unsubFinish = useConverterStore.persist.onFinishHydration(markHydrated); + + // SSG/CSR:store 可能已 merge persist,但 onFinishHydration 未觸發。 + queueMicrotask(() => { + if (isStoreSyncedWithPersist()) { + markHydrated(); + } + }); + + const unsubStore = useConverterStore.subscribe((state, prevState) => { + if (state.lastConverterView !== prevState.lastConverterView && isStoreSyncedWithPersist()) { + markHydrated(); + } }); + + return () => { + active = false; + unsubFinish(); + unsubStore(); + }; }, []); const hasDeepLink = diff --git a/apps/ratewise/src/features/ratewise/components/__tests__/RememberedHomeRoute.test.tsx b/apps/ratewise/src/features/ratewise/components/__tests__/RememberedHomeRoute.test.tsx index 1823e48dd..0b6558302 100644 --- a/apps/ratewise/src/features/ratewise/components/__tests__/RememberedHomeRoute.test.tsx +++ b/apps/ratewise/src/features/ratewise/components/__tests__/RememberedHomeRoute.test.tsx @@ -131,4 +131,18 @@ describe('RememberedHomeRoute', () => { expect(await screen.findByTestId('ratewise-single')).toBeInTheDocument(); expect(screen.queryByTestId('multi-page')).not.toBeInTheDocument(); }); + + it('production MODE:store 已同步 multi 但 hasHydrated=false 時仍導向 /multi', async () => { + vi.stubEnv('MODE', 'production'); + localStorage.setItem(CONVERTER_STORE_KEY, multiPersistPayload); + useConverterStore.setState({ lastConverterView: 'multi' }); + vi.spyOn(useConverterStore.persist, 'hasHydrated').mockReturnValue(false); + + renderHome('/'); + + await waitFor(() => { + expect(screen.getByTestId('multi-page')).toBeInTheDocument(); + }); + vi.unstubAllEnvs(); + }); }); diff --git a/docs/dev/002_development_reward_penalty_log.md b/docs/dev/002_development_reward_penalty_log.md index ab5f8b5f1..079b32343 100644 --- a/docs/dev/002_development_reward_penalty_log.md +++ b/docs/dev/002_development_reward_penalty_log.md @@ -2,7 +2,7 @@ > 版本:outline-v2-ultra > 原則:每筆只保留日期、ID、原因、解法。 -> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+77 +> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+78 ## 新增模板(4 行) @@ -13,6 +13,11 @@ ## 條目(新→舊) +- 日期:2026-06-27 +- ID:reward-cold-start-multi-hydration-fallback +- 原因:RememberedHomeRoute 僅依 hasHydrated/onFinishHydration 設 hydrated,SSG/CSR 生產環境 store 已 merge persist 但回呼未觸發,multi 冷啟動無法還原 +- 解法:hydration effect 補 queueMicrotask 與 store subscribe fallback,store 與 localStorage 同步即標記 hydrated + - 日期:2026-06-27 - ID:reward-applayout-logo-vitest-teardown-flake - 原因:AppLayout.logo.test 渲染後 lazy prompt 非同步觸發 console,vitest onConsoleLog RPC 在 worker 關閉時仍 pending 導致 EnvironmentTeardownError