From 221b567d45e04402c67551134a4adcf59bfca37f Mon Sep 17 00:00:00 2001 From: luciano Date: Thu, 7 May 2026 15:23:56 +0200 Subject: [PATCH] fix(ui): hydration mismatch su /tracking e /plan (date calcolate client-side) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I React errors #418/#423/#425 segnalati dal browser nel flow di salvataggio check-in: l'HTML server-rendered divergeva dal client-rendered, causando `Text content does not match`. La submit del check-in falliva silenziosamente. Cause: - `weight-entry-form.tsx` calcolava `todayISO()` (basato su `new Date()`) dentro `defaultValues` di `useForm`. Il render avviene sia SSR che hydration, e fra i due il valore può differire (anche di pochi ms attraverso la mezzanotte locale, oppure per fuso orario server vs client). - `plan-week.tsx` con `todayDayOfWeek()` aveva lo stesso problema sul pannello macros. Fix: il valore viene calcolato server-side nel rispettivo Server Component (`tracking/page.tsx`, `plan/page.tsx`) e passato come prop al Client Component. Stabile attraverso SSR e hydration perché serializzato nell'HTML iniziale. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/app/[locale]/plan/page.tsx | 10 ++++++++-- apps/web/src/app/[locale]/plan/plan-week.tsx | 12 +++++------ apps/web/src/app/[locale]/tracking/page.tsx | 9 ++++++++- .../[locale]/tracking/weight-entry-form.tsx | 20 +++++++++++-------- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/web/src/app/[locale]/plan/page.tsx b/apps/web/src/app/[locale]/plan/page.tsx index c08dd72..64d7639 100644 --- a/apps/web/src/app/[locale]/plan/page.tsx +++ b/apps/web/src/app/[locale]/plan/page.tsx @@ -26,7 +26,11 @@ export default async function PlanPage({ params: { locale } }: { params: { local const [plan, dailyTarget] = await Promise.all([fetchCurrentPlan(), fetchDailyTarget()]); - return ; + // Calcolato server-side per evitare hydration mismatch nel pannello macros. + const dow = new Date().getDay(); + const todayDayOfWeek = dow === 0 ? 6 : dow - 1; + + return ; } const ITALIAN_DATE = new Intl.DateTimeFormat('it-IT', { @@ -49,9 +53,11 @@ function formatWeek(weekStart: string): string { function PlanPageContent({ plan, dailyTarget, + todayDayOfWeek, }: { plan: Awaited>; dailyTarget: Awaited>; + todayDayOfWeek: number; }) { const t = useTranslations('Plan'); @@ -154,7 +160,7 @@ function PlanPageContent({ {plan ? (
- +
) : null} diff --git a/apps/web/src/app/[locale]/plan/plan-week.tsx b/apps/web/src/app/[locale]/plan/plan-week.tsx index 41a07c1..62363c2 100644 --- a/apps/web/src/app/[locale]/plan/plan-week.tsx +++ b/apps/web/src/app/[locale]/plan/plan-week.tsx @@ -62,18 +62,16 @@ function aggregateMacros(slots: PlanSlot[]): { const FREE_MEAL_DEFAULT_KCAL = 750; -// PRD §6 — il pannello macros laterale ancora oggi (lun=0…dom=6). -function todayDayOfWeek(): number { - const d = new Date().getDay(); // 0 = Sunday - return d === 0 ? 6 : d - 1; -} - export function PlanWeek({ plan, dailyTarget, + todayDayOfWeek, }: { plan: CurrentPlan; dailyTarget: DailyTarget | null; + /** Indice giorno corrente (lun=0…dom=6) calcolato server-side per evitare + * hydration mismatch fra render SSR e client. */ + todayDayOfWeek: number; }) { const t = useTranslations('Plan'); @@ -83,7 +81,7 @@ export function PlanWeek({ byDay.get(slot.dayOfWeek)!.push(slot); } - const today = todayDayOfWeek(); + const today = todayDayOfWeek; const todaySlots = byDay.get(today) ?? []; const todayConsumed = aggregateMacros(todaySlots.filter((s) => s.consumed)); const todayPlanned = aggregateMacros(todaySlots); diff --git a/apps/web/src/app/[locale]/tracking/page.tsx b/apps/web/src/app/[locale]/tracking/page.tsx index aa26717..2694ff3 100644 --- a/apps/web/src/app/[locale]/tracking/page.tsx +++ b/apps/web/src/app/[locale]/tracking/page.tsx @@ -38,11 +38,16 @@ export default async function TrackingPage({ params: { locale } }: { params: { l fetchTodayCheckIn(), ]); + // Calcolato server-side per evitare hydration mismatch su WeightEntryForm. + const now = new Date(); + const todayISO = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + return ( ); } @@ -51,10 +56,12 @@ function TrackingPageContent({ entries, profile, todayCheckIn, + todayISO, }: { entries: Awaited>; profile: ProfileLike; todayCheckIn: Awaited>; + todayISO: string; }) { const t = useTranslations('Tracking'); const latest = entries[0] ?? null; @@ -135,7 +142,7 @@ function TrackingPageContent({

{t('newEntry')}

- +