Use Maptoolkit Maps in Next.js
Next.js renders components on the server first. A WebGL map library cannot run there: it
reaches for window and document while the module is being evaluated, so the import itself
throws before any of your code runs. The fix is to keep the map out of the server bundle
entirely.
The error you get is ReferenceError: window is not defined, and it points at the library
rather than at your component, which sends people looking in the wrong place.
App Router
Mark the map component as client-only, then load it through next/dynamic with server
rendering disabled.
components/MaptoolkitMap.jsx:
"use client";
import { useEffect, useRef } from "react";
import { Map } from "@maptoolkit/maps";
import "@maptoolkit/maps/css";
export default function MaptoolkitMap() {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current) return;
mapRef.current = new Map({
container: containerRef.current,
apiKey: process.env.NEXT_PUBLIC_MAPTOOLKIT_KEY,
center: [16.37, 48.21],
zoom: 12,
});
return () => {
mapRef.current?.remove();
mapRef.current = null;
};
}, []);
return <div ref={containerRef} style={{ width: "100%", height: "400px" }} />;
}app/page.jsx:
import dynamic from "next/dynamic";
const MaptoolkitMap = dynamic(() => import("@/components/MaptoolkitMap"), {
ssr: false,
loading: () => <div style={{ height: 400 }} />,
});
export default function Page() {
return <MaptoolkitMap />;
}"use client" alone is not enough. A client component is still server rendered for the initial
HTML, so the module is still evaluated on the server and still touches window. It is
ssr: false that keeps it out.
In recent Next.js versions ssr: false is rejected inside a Server Component, so the
dynamic() call has to live in a file that is itself a Client Component. Put "use client" at
the top of the file holding the dynamic() call, or wrap the map in a small client-only
component that does the dynamic import.
Pages Router
The same shape, without the directive:
import dynamic from "next/dynamic";
const MaptoolkitMap = dynamic(() => import("../components/MaptoolkitMap"), { ssr: false });
export default function Home() {
return <MaptoolkitMap />;
}The loading placeholder should reserve the height
loading renders while the chunk downloads. If it returns null or an unsized element, the
page reflows when the map appears, which is a visible layout shift and a Core Web Vitals
penalty on a component that is usually the largest thing on the page. Give the placeholder the
same height as the map.
Where the CSS goes
Import the stylesheet in the map component, next to the library. It travels with the dynamic chunk, so it loads when the map does and does not block first paint.
Importing it in app/layout.jsx or _app.jsx instead works, but pulls map CSS into every
route including ones with no map on them.
The API key
NEXT_PUBLIC_ variables are inlined into the client bundle at build time, which is what makes
them readable by the browser and therefore by anyone. That is correct for a map key, since the
browser has to send it with every tile request. Restrict the key to your domains rather than
treating it as a secret.
A key without the NEXT_PUBLIC_ prefix is stripped from the client bundle and arrives as
undefined, which surfaces as a 403 Access denied! from the tile host rather than as a
missing-variable error.
Static export
output: "export" works. The map is client-only, so there is nothing to prerender, and the
dynamic import is resolved in the browser as normal.
Next steps
- Using Maptoolkit maps in React for the lifecycle and cleanup rules the component above follows.
- Maps JS API Reference for the classes used here.
- First Steps for getting and restricting an API key.