SSR-stable today for FlightsMap route (hydration step 1b)
Same pattern as OnlineBoard: route loader supplies todayYyyymmdd() once on the server; FlightsMapStartPage threads it through useMemo dep arrays for searchParams + calendarParams so SSR and client hydration agree on the same dateFrom/dateTo values. Removes the local todayYyyymmdd() copy in favour of the shared util.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, lazy, Suspense, useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { type FC, lazy, Suspense, useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
IMapPopup,
|
||||
IFlightRoute,
|
||||
} from "../types.js";
|
||||
import { todayYyyymmdd } from "@/shared/utils/datetime/index.js";
|
||||
import "./FlightsMapStartPage.scss";
|
||||
|
||||
const MapCanvas = lazy(() =>
|
||||
@@ -47,13 +48,9 @@ const MapCanvas = lazy(() =>
|
||||
// Date helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function todayYyyymmdd(): string {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear().toString();
|
||||
const m = (now.getMonth() + 1).toString().padStart(2, "0");
|
||||
const d = now.getDate().toString().padStart(2, "0");
|
||||
return `${y}${m}${d}`;
|
||||
}
|
||||
// todayYyyymmdd lives in @/shared/utils/datetime — imported below — so the
|
||||
// SSR loader and the client component agree on a single value carried via
|
||||
// _ROUTER_DATA. The local helper here only handles month arithmetic.
|
||||
|
||||
function addMonthsYyyymmdd(base: string, months: number): string {
|
||||
const y = Number(base.slice(0, 4));
|
||||
@@ -79,11 +76,24 @@ export interface FlightsMapStartPageProps {
|
||||
* fetch tiles from an upstream tile service (e.g. flights.test.aeroflot.ru).
|
||||
*/
|
||||
tileUrl?: string;
|
||||
/**
|
||||
* yyyyMMdd of "today" computed once on the SSR server (route loader)
|
||||
* and passed in. See OnlineBoardFilterProps.today — same purpose:
|
||||
* eliminate `new Date()` calls during render that diverge between SSR
|
||||
* and CSR and trigger React hydration error #423.
|
||||
*/
|
||||
today?: string;
|
||||
}
|
||||
|
||||
export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
||||
tileUrl: tileUrlProp,
|
||||
today,
|
||||
}) => {
|
||||
// Capture a single fallback today for unit tests that don't wire the
|
||||
// route loader. Production always supplies the prop; the ref is unused
|
||||
// there but always called to keep hook order stable.
|
||||
const fallbackTodayYmd = useRef(todayYyyymmdd()).current;
|
||||
const todayYmd = today ?? fallbackTodayYmd;
|
||||
const { t } = useTranslation();
|
||||
const { language } = useLocale();
|
||||
|
||||
@@ -145,28 +155,26 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
||||
const searchParams = useMemo<FlightsMapSearchParams | null>(() => {
|
||||
if (!filterState.departure) return null;
|
||||
|
||||
const today = todayYyyymmdd();
|
||||
return {
|
||||
departure: filterState.departure,
|
||||
arrival: filterState.arrival,
|
||||
dateFrom: today,
|
||||
dateTo: addMonthsYyyymmdd(today, 6),
|
||||
dateFrom: todayYmd,
|
||||
dateTo: addMonthsYyyymmdd(todayYmd, 6),
|
||||
connections: effectiveConnections,
|
||||
};
|
||||
}, [filterState.departure, filterState.arrival, effectiveConnections]);
|
||||
}, [filterState.departure, filterState.arrival, effectiveConnections, todayYmd]);
|
||||
|
||||
// Build calendar params
|
||||
const calendarParams = useMemo<FlightsMapCalendarParams | null>(() => {
|
||||
if (!filterState.departure) return null;
|
||||
|
||||
const today = todayYyyymmdd();
|
||||
return {
|
||||
date: today,
|
||||
date: todayYmd,
|
||||
departure: filterState.departure,
|
||||
arrival: filterState.arrival,
|
||||
connections: filterState.connections,
|
||||
};
|
||||
}, [filterState.departure, filterState.arrival, filterState.connections]);
|
||||
}, [filterState.departure, filterState.arrival, filterState.connections, todayYmd]);
|
||||
|
||||
const { routes, loading, error } = useFlightsMapSearch(searchParams);
|
||||
const { availableDays } = useFlightsMapCalendar(calendarParams);
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useParams, useLoaderData } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
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(
|
||||
@@ -22,6 +23,11 @@ const FlightsMapStartPage = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
/** SSR-stable today; see src/routes/[lang]/onlineboard/page.tsx loader. */
|
||||
export const loader = (): { today: string } => ({
|
||||
today: todayYyyymmdd(),
|
||||
});
|
||||
|
||||
export default function FlightsMapPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
@@ -31,6 +37,7 @@ 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 isEnabled = useFeatureFlag("flightsMap");
|
||||
|
||||
if (!isEnabled) {
|
||||
@@ -48,7 +55,7 @@ export default function FlightsMapPage(): JSX.Element {
|
||||
<>
|
||||
<SeoHead {...seoProps} jsonLd={jsonLd} />
|
||||
<Suspense fallback={<div aria-busy="true">{t("SHARED.LOADING")}</div>}>
|
||||
<FlightsMapStartPage tileUrl={tileUrl} />
|
||||
<FlightsMapStartPage tileUrl={tileUrl} today={today} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user