Skip to content
MapLibre GL JS

Use Maptoolkit with react-map-gl and MapLibre GL JS

react-map-gl wraps MapLibre GL JS in React components. A Maptoolkit style URL goes into its mapStyle prop, and that is the whole integration: the style names its own tile sources and carries its own attribution.

Install

npm install react-map-gl maplibre-gl

react-map-gl 8 supports both MapLibre and Mapbox, so you choose the library in the import path. Import from react-map-gl/maplibre. The plain import Map from "react-map-gl" that older tutorials show was removed in version 8 and fails to resolve.

@vis.gl/react-maplibre is the same MapLibre code as a separate package; react-map-gl 8 depends on it and re-exports it. Use either one, the components are identical.

The component

import "./maplibre-worker";
import Map, { NavigationControl, Marker } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";

const KEY = import.meta.env.VITE_MAPTOOLKIT_KEY;

export default function MaptoolkitMap() {
  return (
    <Map
      initialViewState={{ longitude: 11.4041, latitude: 47.2692, zoom: 12 }}
      style={{ width: "100%", height: "400px" }}
      mapStyle={`https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${KEY}`}
      attributionControl={{ compact: false }}
    >
      <NavigationControl position="top-right" />
      <Marker longitude={11.4041} latitude={47.2692} />
    </Map>
  );
}

The first import is the worker setup described in the next section.

  • The API key travels in the style URL. MapLibre reuses it for every tile request the style triggers, so there is no separate tile configuration.
  • initialViewState is read once. It sets the starting position and then the user is in control. To drive the position from React state instead, pass longitude, latitude and zoom as props with an onMove handler; that makes the map fully controlled.
  • attributionControl={{ compact: false }} keeps the credit visible. By default MapLibre collapses the attribution into an icon as soon as the user interacts with the map, which hides the credit the license requires you to show.
  • The container needs a height. A map with no height renders nothing and reports no error.

The style id follows maptoolkit.<variant>, and Standard Map Styles lists the seven variants.

MapLibre 6 needs its worker set up

With maplibre-gl 6, the map stays blank after bundling and the console shows Worker failed to load. In the Vite development server the request for maplibre-gl-worker.mjs returns 404.

MapLibre 6 ships its web worker as a separate file, dist/maplibre-gl-worker.mjs, and finds it next to its own script at runtime. A bundler moves and renames the library, and that location no longer exists. setWorkerUrl() tells MapLibre where the worker is. With Vite, the ?worker&url import builds the worker together with the files it imports and returns its final URL:

// maplibre-worker.js
import { setWorkerUrl } from "maplibre-gl";
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";

setWorkerUrl(workerUrl);

Import this module once, before the first map is created, as the component above does. The same fix covers both the development server and the production build. With another bundler the idea is the same: have it emit the worker file as its own bundle, then pass that file’s URL to setWorkerUrl().

Adding your own data

Source and Layer components map one to one onto MapLibre sources and layers. They are added once the style has loaded, so there is no load event to wait for yourself:

import Map, { Source, Layer } from "react-map-gl/maplibre";

const route = {
  type: "Feature",
  geometry: {
    type: "LineString",
    coordinates: [[11.3928, 47.2654], [11.4041, 47.2692], [11.4205, 47.2731]],
  },
};

<Map initialViewState={viewState} style={{ height: "400px" }} mapStyle={styleUrl}>
  <Source id="route" type="geojson" data={route}>
    <Layer id="route" type="line" paint={{ "line-color": "#d6336c", "line-width": 5 }} />
  </Source>
</Map>

Pass new data to Source and react-map-gl updates the existing source with setData().

Calling the map directly

For anything the props do not cover, such as animated camera moves, attach a ref. The ref exposes the camera methods directly, and getMap() returns the underlying MapLibre map:

import { useRef } from "react";

const mapRef = useRef(null);

<button onClick={() => mapRef.current?.flyTo({ center: [13.055, 47.8095], zoom: 12 })}>
  Salzburg
</button>
<Map ref={mapRef} initialViewState={viewState} mapStyle={styleUrl} style={{ height: "400px" }} />

react-map-gl creates and removes the MapLibre map itself, so there is no cleanup to write and no guard against React’s StrictMode double mount. With Maps JS or plain MapLibre in React, both are your job.

Next steps

The component above is a basemap with a marker. The per-API MapLibre examples show what goes on top, and their layer definitions move into Source and Layer unchanged: a route from the Routing API, or 3D relief from Terrain Tiles.

Pick the style per use case before you style anything yourself. Hiking and Winter carry hillshading and read as mountain maps, and Street is built for dense city labels.