Appearance
Embed with React and Next.js
Use the Mapsted Maps JavaScript API inside a React 18 or Next.js 14+ application. This tutorial covers the useEffect + useRef mount pattern, React 18 StrictMode safety, Next.js App Router SSR compatibility, and the 'use client' directive.
Prerequisites
- Node.js 20 or later
- npm 9 or later
- React 18+ (plain Vite project) or Next.js 14+ (App Router)
- Basic familiarity with React hooks
1. Install the API
bash
npm install @mapsted/maps-js-apiLicensed customers
The @mapsted/maps-js-api package is currently access-restricted on npm. If npm install returns 404 or 401, contact your Mapsted account manager for an npm token, then run npm config set //registry.npmjs.org/:_authToken <token> before re-running install.
2. React component pattern (Vite + plain React 18)
Create a MapstedMap.tsx component. Use useRef to hold a reference to the DOM container and useEffect to initialize and destroy the map instance.
tsx
// src/components/MapstedMap.tsx
import { useEffect, useRef } from 'react';
import { init } from '@mapsted/maps-js-api';
import type { MapInstance } from '@mapsted/maps-js-api';
interface MapstedMapProps {
propertyId: number;
}
export function MapstedMap({ propertyId }: MapstedMapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const instanceRef = useRef<MapInstance | null>(null);
useEffect(() => {
if (!containerRef.current) return;
let cancelled = false;
init({
element: containerRef.current,
propertyId,
onload: () => {
if (!cancelled) {
console.log('Mapsted map ready');
}
},
}).then((instance) => {
if (cancelled) {
// StrictMode remounted before init resolved — clean up immediately
instance.destroy();
return;
}
instanceRef.current = instance;
});
// Cleanup runs on unmount (and on the first StrictMode remount cycle)
return () => {
cancelled = true;
instanceRef.current?.destroy();
instanceRef.current = null;
};
}, [propertyId]);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '100vh' }}
/>
);
}Use it in your app:
tsx
// src/App.tsx
import { MapstedMap } from './components/MapstedMap';
export default function App() {
return <MapstedMap propertyId={1643} />;
}React 18 StrictMode notes
React 18 StrictMode intentionally mounts, unmounts, then remounts every component in development to help you find missing cleanup code. The pattern above guards against this:
- The
cancelledflag catches the case where the cleanup function runs beforeinit()resolves. - If
init()resolves after cleanup,instance.destroy()is called immediately so no orphaned iframe is left behind. - In production, StrictMode double-invocation does not occur — there is no performance cost.
3. Next.js App Router
The Mapsted API is browser-only. It attaches event listeners to window and creates an <iframe>. Both operations fail during server-side rendering, so you must prevent the component from running on the server.
Option A — 'use client' in a dedicated file
Place the component file in app/components/MapstedMap.tsx and add 'use client' at the top:
tsx
// app/components/MapstedMap.tsx
'use client';
import { useEffect, useRef } from 'react';
import { init } from '@mapsted/maps-js-api';
import type { MapInstance } from '@mapsted/maps-js-api';
interface MapstedMapProps {
propertyId: number;
}
export function MapstedMap({ propertyId }: MapstedMapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const instanceRef = useRef<MapInstance | null>(null);
useEffect(() => {
if (!containerRef.current) return;
let cancelled = false;
init({
element: containerRef.current,
propertyId,
}).then((instance) => {
if (cancelled) { instance.destroy(); return; }
instanceRef.current = instance;
});
return () => {
cancelled = true;
instanceRef.current?.destroy();
instanceRef.current = null;
};
}, [propertyId]);
return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />;
}Then import it from a Server Component — it renders the client boundary automatically:
tsx
// app/page.tsx (Server Component — no 'use client' needed here)
import { MapstedMap } from './components/MapstedMap';
export default function Page() {
return (
<main>
<MapstedMap propertyId={1643} />
</main>
);
}Option B — dynamic() import with ssr: false
If you prefer to keep the import in a Server Component file, use Next.js dynamic():
tsx
// app/page.tsx (Server Component)
import dynamic from 'next/dynamic';
const MapstedMap = dynamic(
() => import('./components/MapstedMap').then((m) => m.MapstedMap),
{ ssr: false }
);
export default function Page() {
return (
<main>
<MapstedMap propertyId={1643} />
</main>
);
}{ ssr: false } tells Next.js to skip this component during the server render pass entirely, so none of the browser-dependent code runs on the server.
Which option should I use?
Both options produce the same output. Option A ('use client') is the idiomatic App Router approach and makes the client boundary explicit. Option B (dynamic) is useful when the parent must stay a Server Component for caching or data-fetching reasons.
4. Server-side rendering caveat
The Mapsted Maps JavaScript API imports a number of browser-only globals (window, document, MessageChannel). If you have a custom SSR setup that pre-renders your React tree without using 'use client' or dynamic, add a runtime guard:
tsx
// Only call init in browser environments
if (typeof window !== 'undefined') {
init({
element: containerRef.current,
propertyId: 1643,
}).catch(console.error); // fire-and-forget: surface init errors in the console
}Why no await here?
The if (typeof window !== 'undefined') guard is placed outside any async function, so init() is called as fire-and-forget. .catch(console.error) ensures that initialization errors are not silently swallowed. If you are inside an async function, prefer await init(...) and handle errors with try/catch.
instanceRef cleanup
The useEffect cleanup function (return () => { ... }) must be synchronous — React's TypeScript signature for the cleanup callback is () => void, which means it cannot be async and the returned Promise would be ignored. Calling instanceRef.current?.destroy() without await is intentional: the cleanup fires the destroy call synchronously and the map tears down asynchronously in the background. This is safe because the cleanup only needs to initiate destruction, not await its completion.
This guard is not needed when using 'use client' or dynamic({ ssr: false }) — both patterns already skip server execution.
5. Live sandbox preview
The sandbox below runs the same map using the CDN-global API, showing what your component will render:
6. Custom hook recipe
For a reusable useMapstedMap() hook that wraps the pattern above, see the companion guide: useMapstedMap() hook recipe
Next steps
- Subscribe to map events: Map events
- Add markers and popups: Markers and popups
- Prefer Vue instead? See Embed with Vue 3 and Nuxt
- Prefer Angular? See Embed with Angular