Add ClientMap wrapper to avoid lazy() SSR resolution

.client.tsx modules return undefined on the server, but lazy() still
tries to resolve them during SSR causing "Element type is invalid".
Replace lazy()+Suspense with a ClientMap component that dynamically
imports the map only in useEffect (client-side only).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-05 20:10:57 +02:00
parent 81eb38a24d
commit 9b2adebc4d
5 changed files with 30 additions and 32 deletions

View file

@ -0,0 +1,21 @@
import { useState, useEffect, type ComponentProps } from "react";
type MapProps = ComponentProps<typeof import("./RouteMapThumbnail.client").RouteMapThumbnail>;
export function ClientMap(props: MapProps) {
const [Component, setComponent] = useState<React.ComponentType<MapProps>>();
useEffect(() => {
import("./RouteMapThumbnail.client").then((m) => setComponent(() => m.RouteMapThumbnail));
}, []);
if (!Component) {
return (
<div className={`flex items-center justify-center bg-gray-100 text-gray-500 ${props.className ?? "h-36 w-full rounded"}`}>
Loading map...
</div>
);
}
return <Component {...props} />;
}

View file

@ -1,4 +1,3 @@
import { Suspense, lazy } from "react";
import { data, redirect } from "react-router";
import { useTranslation } from "react-i18next";
import type { Route } from "./+types/activities.$id";
@ -7,10 +6,7 @@ import { getActivity, deleteActivity, linkActivityToRoute, createRouteFromActivi
import { deleteImportByActivity } from "~/lib/sync/imports.server";
import { listRoutes } from "~/lib/routes.server";
import { ClientDate } from "~/components/ClientDate";
const RouteMapThumbnail = lazy(() =>
import("~/components/RouteMapThumbnail.client").then((m) => ({ default: m.RouteMapThumbnail })),
);
import { ClientMap } from "~/components/ClientMap";
export async function loader({ params, request }: Route.LoaderArgs) {
const activity = await getActivity(params.id);
@ -125,9 +121,7 @@ export default function ActivityDetailPage({ loaderData }: Route.ComponentProps)
{activity.geojson && (
<div className="mt-6 overflow-hidden rounded-lg border border-gray-200" style={{ height: 400 }}>
<Suspense fallback={<div className="flex h-full items-center justify-center bg-gray-100 text-gray-500">Loading map...</div>}>
<RouteMapThumbnail geojson={activity.geojson} interactive className="h-full w-full" />
</Suspense>
<ClientMap geojson={activity.geojson} interactive className="h-full w-full" />
</div>
)}

View file

@ -1,14 +1,10 @@
import { data, redirect } from "react-router";
import { Suspense, lazy } from "react";
import { useTranslation } from "react-i18next";
import type { Route } from "./+types/activities._index";
import { getSessionUser } from "~/lib/auth.server";
import { listActivities } from "~/lib/activities.server";
import { ClientDate } from "~/components/ClientDate";
const RouteMapThumbnail = lazy(() =>
import("~/components/RouteMapThumbnail.client").then((m) => ({ default: m.RouteMapThumbnail })),
);
import { ClientMap } from "~/components/ClientMap";
export async function loader({ request }: Route.LoaderArgs) {
const user = await getSessionUser(request);
@ -64,9 +60,7 @@ export default function ActivitiesListPage({ loaderData }: Route.ComponentProps)
<div className="flex gap-4">
<div className="w-48 shrink-0">
{activity.geojson ? (
<Suspense fallback={<div className="h-36 w-full animate-pulse rounded bg-gray-100" />}>
<RouteMapThumbnail geojson={activity.geojson} />
</Suspense>
<ClientMap geojson={activity.geojson} />
) : (
<div className="flex h-36 w-full items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
{t("routes.noMapPreview")}

View file

@ -1,14 +1,11 @@
import { useState, useCallback, Suspense, lazy } from "react";
import { useState, useCallback } from "react";
import { data, redirect } from "react-router";
import { useTranslation } from "react-i18next";
import type { Route } from "./+types/routes.$id";
import { getSessionUser } from "~/lib/auth.server";
import { getRoute, getRouteWithVersions, deleteRoute, updateRoute } from "~/lib/routes.server";
import { ClientDate } from "~/components/ClientDate";
const RouteMapThumbnail = lazy(() =>
import("~/components/RouteMapThumbnail.client").then((m) => ({ default: m.RouteMapThumbnail })),
);
import { ClientMap } from "~/components/ClientMap";
export async function loader({ params, request }: Route.LoaderArgs) {
@ -161,9 +158,7 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {
{route.geojson && (
<div className="mt-6 overflow-hidden rounded-lg border border-gray-200" style={{ height: 400 }}>
<Suspense fallback={<div className="flex h-full items-center justify-center bg-gray-100 text-gray-500">Loading map...</div>}>
<RouteMapThumbnail geojson={route.geojson} interactive className="h-full w-full" />
</Suspense>
<ClientMap geojson={route.geojson} interactive className="h-full w-full" />
</div>
)}

View file

@ -1,14 +1,10 @@
import { data, redirect } from "react-router";
import { Suspense, lazy } from "react";
import { useTranslation } from "react-i18next";
import type { Route } from "./+types/routes._index";
import { getSessionUser } from "~/lib/auth.server";
import { listRoutes } from "~/lib/routes.server";
import { ClientDate } from "~/components/ClientDate";
const RouteMapThumbnail = lazy(() =>
import("~/components/RouteMapThumbnail.client").then((m) => ({ default: m.RouteMapThumbnail })),
);
import { ClientMap } from "~/components/ClientMap";
export async function loader({ request }: Route.LoaderArgs) {
const user = await getSessionUser(request);
@ -62,9 +58,7 @@ export default function RoutesListPage({ loaderData }: Route.ComponentProps) {
<div className="flex gap-4">
<div className="w-48 shrink-0">
{route.geojson ? (
<Suspense fallback={<div className="h-36 w-full animate-pulse rounded bg-gray-100" />}>
<RouteMapThumbnail geojson={route.geojson} />
</Suspense>
<ClientMap geojson={route.geojson} />
) : (
<div className="flex h-36 w-full items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
{t("routes.noMapPreview")}