From bfd236cf8924ca87e3ea5fd275b14fd37df085c0 Mon Sep 17 00:00:00 2001 From: gnezim Date: Mon, 27 Apr 2026 20:20:18 +0300 Subject: [PATCH] Move SSR-stable today loader to data.ts (Modern.js convention) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline export const loader from page.tsx didn't run — _ROUTER_DATA showed loaderData[(lang)/onlineboard/page] = null and useLoaderData() threw 'Cannot read properties of null'. Modern.js conventional routes require the loader in a co-located data.ts file. useLoaderData() now defensively handles null (defaults to undefined, component falls back to useRef(new Date())). Worst case if loader still doesn't fire: same hydration drift as before, no crash. --- src/routes/[lang]/flights-map/data.ts | 6 ++++++ src/routes/[lang]/flights-map/page.tsx | 12 ++++-------- src/routes/[lang]/onlineboard/data.ts | 10 ++++++++++ src/routes/[lang]/onlineboard/page.tsx | 20 ++++++-------------- 4 files changed, 26 insertions(+), 22 deletions(-) create mode 100644 src/routes/[lang]/flights-map/data.ts create mode 100644 src/routes/[lang]/onlineboard/data.ts diff --git a/src/routes/[lang]/flights-map/data.ts b/src/routes/[lang]/flights-map/data.ts new file mode 100644 index 00000000..a29be3f5 --- /dev/null +++ b/src/routes/[lang]/flights-map/data.ts @@ -0,0 +1,6 @@ +import { todayYyyymmdd } from "@/shared/utils/datetime/index.js"; + +/** Modern.js conventional data loader; see /onlineboard/data.ts. */ +export const loader = (): { today: string } => ({ + today: todayYyyymmdd(), +}); diff --git a/src/routes/[lang]/flights-map/page.tsx b/src/routes/[lang]/flights-map/page.tsx index 6df8eab9..7408fd7a 100644 --- a/src/routes/[lang]/flights-map/page.tsx +++ b/src/routes/[lang]/flights-map/page.tsx @@ -15,18 +15,13 @@ import { buildFlightsMapSeo } from "@/features/flights-map/seo.js"; import { buildFlightsMapJsonLd } from "@/features/flights-map/json-ld.js"; import { useFeatureFlag } from "@/features/flights-map/hooks/useFeatureFlag.js"; import { getEnv } from "@/env/index.js"; -import { todayYyyymmdd } from "@/shared/utils/datetime/index.js"; - const FlightsMapStartPage = lazy(() => import("@/features/flights-map/components/FlightsMapStartPage.js").then( (m) => ({ default: m.FlightsMapStartPage }), ), ); -/** SSR-stable today; see src/routes/[lang]/onlineboard/page.tsx loader. */ -export const loader = (): { today: string } => ({ - today: todayYyyymmdd(), -}); +// Loader lives in ./data.ts (Modern.js conventional data file). export default function FlightsMapPage(): JSX.Element { const { t } = useTranslation(); @@ -37,7 +32,8 @@ export default function FlightsMapPage(): JSX.Element { // Tile URL read on the server (where process.env is available) and passed // down as a prop — the client bundle can't read MAP_TILE_URL itself. const tileUrl = env.MAP_TILE_URL; - const { today } = useLoaderData() as { today: string }; + const data = useLoaderData() as { today?: string } | null; + const today = data?.today; const isEnabled = useFeatureFlag("flightsMap"); if (!isEnabled) { @@ -55,7 +51,7 @@ export default function FlightsMapPage(): JSX.Element { <> {t("SHARED.LOADING")}}> - + ); diff --git a/src/routes/[lang]/onlineboard/data.ts b/src/routes/[lang]/onlineboard/data.ts new file mode 100644 index 00000000..4bbdb8a0 --- /dev/null +++ b/src/routes/[lang]/onlineboard/data.ts @@ -0,0 +1,10 @@ +import { todayYyyymmdd } from "@/shared/utils/datetime/index.js"; + +/** + * Modern.js conventional data loader. Co-located with page.tsx. + * Computes today on the server, exposes it via useLoaderData() so SSR + * and client hydration agree on the same yyyyMMdd. + */ +export const loader = (): { today: string } => ({ + today: todayYyyymmdd(), +}); diff --git a/src/routes/[lang]/onlineboard/page.tsx b/src/routes/[lang]/onlineboard/page.tsx index 399c51d2..857c6c19 100644 --- a/src/routes/[lang]/onlineboard/page.tsx +++ b/src/routes/[lang]/onlineboard/page.tsx @@ -11,31 +11,23 @@ import { useTranslation } from "@/i18n/provider.js"; import { SeoHead } from "@/ui/seo/SeoHead.js"; import { buildOnlineBoardStartSeo } from "@/features/online-board/seo.js"; import { getEnv } from "@/env/index.js"; -import { todayYyyymmdd } from "@/shared/utils/datetime/index.js"; - const OnlineBoardStartPage = lazy(() => import("@/features/online-board/components/OnlineBoardStartPage.js").then( (m) => ({ default: m.OnlineBoardStartPage }), ), ); -/** - * Compute "today" once on the server. The result rides _ROUTER_DATA into - * the client bundle, so the first client render — the one that hydrates - * the SSR markup — sees the same yyyyMMdd value the server saw. Without - * this, components calling `new Date()` during render produce different - * markup on the two sides and React throws hydration error #423. - */ -export const loader = (): { today: string } => ({ - today: todayYyyymmdd(), -}); +// Loader lives in ./data.ts (Modern.js conventional data file). Result +// rides _ROUTER_DATA into the client so SSR and hydration agree on the +// same `today` value, eliminating render-path Date() drift (React #423). export default function OnlineBoardPage(): JSX.Element { const { t } = useTranslation(); const routeParams = useParams<{ lang: string }>(); const locale = routeParams.lang ?? "ru-ru"; const canonicalOrigin = getEnv().PROD_ORIGIN; - const { today } = useLoaderData() as { today: string }; + const data = useLoaderData() as { today?: string } | null; + const today = data?.today; const seoProps = buildOnlineBoardStartSeo(t, locale, canonicalOrigin); @@ -43,7 +35,7 @@ export default function OnlineBoardPage(): JSX.Element { <> {t("SHARED.LOADING")}}> - + );