Home / Blog / Why Map.tsx is fourteen lines

May 11, 2026· Wine World Map

Why Map.tsx is fourteen lines

The file components/Map.tsx is the entry point for the entire interactive map. Here it is in its entirety:

'use client'

import dynamic from 'next/dynamic'

const MapComponent = dynamic(() => import('./MapComponent'), {
  ssr: false,
  loading: () => (
    <div className="w-full h-full flex items-center justify-center bg-[#0d0407]">
      <div className="text-[#c9a84c] font-serif text-lg animate-pulse">Laddar karta…</div>
    </div>
  ),
})

export default MapComponent

Fourteen lines, of which seven are the loading spinner. The actual map — MapComponent.tsx — is 534 lines: marker clustering, layer switching, click handlers, flyTo animations, a pulsing-ring highlight for search results. None of that ships with Map.tsx itself.

The reason is the seven-letter argument ssr: false.

Why Leaflet can't be server-rendered

Leaflet, the library that draws the actual map, touches window the moment it's imported:

// from leaflet's source, paraphrased
L.Browser = (function () {
  const ua = navigator.userAgent.toLowerCase()
  // ...feature detection
})()

That navigator.userAgent line runs at module-evaluation time, not inside a function. So import 'leaflet' is enough to throw ReferenceError: navigator is not defined in a Node.js environment — which is exactly where Next.js renders Server Components.

You can't lazy-import inside MapComponent.tsx either, because the component file itself has import 'leaflet' at the top. The import has to be moved out of any code path that runs on the server, period.

What dynamic(..., { ssr: false }) actually does

It splits the bundle. MapComponent.tsx and everything it imports (Leaflet, markercluster, the CSS) gets emitted as a separate JS chunk that's only fetched client-side. The server renders the loading fallback. Hydration on the client then triggers the chunk load, and once it arrives, React swaps the spinner for the map.

The bundle-split is also why this is desirable even when SSR could work: Leaflet plus markercluster plus their CSS is roughly 160 KB compressed. Sites that don't need the map (most blog pageviews) never download it.

The cost

Two things suffer:

  1. First-paint shows the spinner, not the map. On a fast connection this is ~200 ms. On 3G it's a couple of seconds. The alternative is a server-rendered placeholder that looks like a map but isn't, which is worse — visitors click it and nothing happens.

  2. Search engines don't see the map. Googlebot does a JS-aware crawl these days, but it's not guaranteed to wait for the dynamic chunk to load. For the /map route this is fine because the SEO value lives in the per-region landing pages, which are server-rendered.

The 'use client' line is doing real work

That 'use client' at the top of the file is the seam between Server Components (the default in Next.js App Router) and Client Components. Everything imported from a Client Component is also a client boundary. The dynamic() call only works in a Client Component, because it relies on React.lazy semantics.

If we removed 'use client', the import of next/dynamic would fail to typecheck. If we kept 'use client' but inlined the Leaflet import here instead of through dynamic, the server build would crash at compile time. The fourteen lines are load-bearing in both directions.

When the fourteen-line file pays off

Every time we change something inside the map — adding a layer, tuning the cluster radius, fixing a bug in flyTo — we touch MapComponent.tsx. We never touch Map.tsx. The boundary stays stable. The 534-line file gets edited weekly; the 14-line file gets edited approximately never.

That's the unglamorous design payoff of putting a thin shim around a heavy dependency: the shim absorbs the SSR weirdness, and the heavy file gets to be a plain old client component that imports whatever it wants.

#nextjs#leaflet#ssr