Move SSR-stable today loader to data.ts (Modern.js convention)
ci-deploy / build-deploy-test (push) Successful in 1m54s

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.
This commit is contained in:
2026-04-27 20:20:18 +03:00
parent a412e857f4
commit bfd236cf89
4 changed files with 26 additions and 22 deletions
+6
View File
@@ -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(),
});
+4 -8
View File
@@ -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 {
<>
<SeoHead {...seoProps} jsonLd={jsonLd} />
<Suspense fallback={<div aria-busy="true">{t("SHARED.LOADING")}</div>}>
<FlightsMapStartPage tileUrl={tileUrl} today={today} />
<FlightsMapStartPage tileUrl={tileUrl} {...(today ? { today } : {})} />
</Suspense>
</>
);
+10
View File
@@ -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(),
});
+6 -14
View File
@@ -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 {
<>
<SeoHead {...seoProps} />
<Suspense fallback={<div aria-busy="true">{t("SHARED.LOADING")}</div>}>
<OnlineBoardStartPage today={today} />
<OnlineBoardStartPage {...(today ? { today } : {})} />
</Suspense>
</>
);