react-map-gl (MapLibre)
Conventions for react-map-gl/maplibre in FixMyBerlin / FMC projects. Import from react-map-gl/maplibre (not react-map-gl alone). Always import maplibre-gl/dist/maplibre-gl.css.
When to apply
- Adding or changing
<Map>, <Source>, <Layer>, controls, markers
- Click/hover/feature inspection wired to map events
- Syncing viewport to URL (
map=zoom/lat/lng)
- Child components that need the map instance (feature state, fitBounds, queryRenderedFeatures)
- Debugging “map half loaded” errors, missing style images, or wrong click targets
- Highlighting selected/hovered features (
setFeatureState vs layer filter)
- Reviewing code that uses
useEffect to sync map camera, clicks, or hover
- Code calling
map.addSource, map.addLayer, or effect-driven layer lifecycle
- Agent or E2E inspection of layer order, zoom, or rendered features (
window.__mainMap)
Reference reading order
- declarative-source-layer.md —
<Source> / <Layer> only; never map.addSource / map.addLayer
- map-event-handlers.md — which
<Map> handler for which use case; not useEffect
- map-provider-wrapper.md — MapProvider parent; useMap(); never manual refs
- interactive-layer-ids.md —
interactiveLayerIds + event.features (not raw MapLibre)
- map-loaded-hook.md — guard map API calls until
onLoad / useMapLoaded()
- map-debug-exposure.md —
window.__mainMap in dev/Playwright; exposeMainMapForDebugging in onLoad
- feature-state.md —
setFeatureState vs React filter; reset/diff; inspector sync
- flat-source-layer.md — sibling
<Source> + <Layer>, required props
- layer-visibility-vs-unmount.md —
layout.visibility vs conditional render
- map-props-attribution-locale.md —
attributionControl={false}, locale, RTLTextPlugin
- cursor-handling.md —
cursor prop from hover state
- initial-view-state.md — uncontrolled
initialViewState vs URL-driven vs bounds
- map-url-state.md — Map ↔ URL sync (
onMoveEnd); URL format → tanstack-router-conventions → map-search-param.md
- map-images-missing.md —
styleimagemissing + dynamic icons
- map-images-proactive.md — proactive
addImage for known sprites (vzk-bw)
Pair with: skill tanstack-router-conventions (?map= URL contract), skill nuqs (legacy URL parsers), skill zustand-state-management (map UI store like useMapLoaded), skill playwright-skill (E2E map helpers, getMapLayerIds).
Non-negotiable rules
| Topic |
Rule |
| Sources / layers |
<Source> + <Layer> in JSX only. Never map.addSource / map.addLayer (or remove*) for app data. Show/hide via layout.visibility or conditional render — see declarative-source-layer.md. |
| Map events |
Camera, pointer, load, and tile lifecycle → <Map> callback props. Not useEffect + map.on(…) for the same behavior. |
| Map access |
Wrap app map UI in <MapProvider>. Use useMap() keyed by <Map id="…">. Never pass ref to <Map> for child access. |
| Map ready |
Guard getStyle, queryRenderedFeatures, setFeatureState, etc. with useMapLoaded() (set in onLoad). |
| Debug exposure |
In onLoad, call exposeMainMapForDebugging(event.target) (dev + Playwright only). Agents/tests read window.__mainMap — see map-debug-exposure.md. |
| Layers |
Flat siblings: one <Source> per source id, then <Layer> siblings (not nested layers under Source for refactorability). |
| Clicks |
Put layer ids in interactiveLayerIds; read event.features in handlers — do not call queryRenderedFeatures for primary click picking unless syncing URL features. |
| Feature highlight |
Inspector/form truth in React; map tint via setFeatureState (paint) or React filter layer — see feature-state.md. Clear old state before applying new. |
| Attribution |
attributionControl={false} on <Map>; add <AttributionControl compact> as child. compact is not start-closed — see map-props-attribution-locale.md. |
| Viewport URL |
Serialize zoom/lat/lng with zoom-aware lat/lng rounding; write on onMoveEnd with history: 'replace'. |
| View state |
Prefer uncontrolled initialViewState seeded from URL; do not mirror full viewState in React state unless you need programmatic camera control. |
Quick import
import 'maplibre-gl/dist/maplibre-gl.css'
import { MapProvider, Map, Source, Layer, useMap } from 'react-map-gl/maplibre'
1---2name: react-map-gl3description: react-map-gl/maplibre patterns for FMC geo apps: declarative Source/Layer (never addSource/addLayer), MapProvider + useMap(), interactiveLayerIds, flat Source/Layer, map-loaded guard, window.__mainMap debug exposure, Map event handlers (not useEffect), URL map state, initialViewState, cursor, feature state, missing images. Use when building or reviewing Map components, layers, click/hover handlers, setFeatureState, map URL sync, or agent/test map inspection in TanStack Start / Vite apps. Reference implementation: tilda-geo/app.4---56# react-map-gl (MapLibre)78Conventions for `react-map-gl/maplibre` in FixMyBerlin / FMC projects. Import from `react-map-gl/maplibre` (not `react-map-gl` alone). Always import `maplibre-gl/dist/maplibre-gl.css`.910## When to apply1112- Adding or changing `<Map>`, `<Source>`, `<Layer>`, controls, markers13- Click/hover/feature inspection wired to map events14- Syncing viewport to URL (`map=zoom/lat/lng`)15- Child components that need the map instance (feature state, fitBounds, queryRenderedFeatures)16- Debugging “map half loaded” errors, missing style images, or wrong click targets17- Highlighting selected/hovered features (`setFeatureState` vs layer `filter`)18- Reviewing code that uses `useEffect` to sync map camera, clicks, or hover19- Code calling `map.addSource`, `map.addLayer`, or effect-driven layer lifecycle20- Agent or E2E inspection of layer order, zoom, or rendered features (`window.__mainMap`)2122## Reference reading order23241. [declarative-source-layer.md](references/declarative-source-layer.md) — **`<Source>` / `<Layer>` only; never `map.addSource` / `map.addLayer`**252. [map-event-handlers.md](references/map-event-handlers.md) — **which `<Map>` handler for which use case; not useEffect**263. [map-provider-wrapper.md](references/map-provider-wrapper.md) — **MapProvider parent; useMap(); never manual refs**274. [interactive-layer-ids.md](references/interactive-layer-ids.md) — `interactiveLayerIds` + `event.features` (not raw MapLibre)285. [map-loaded-hook.md](references/map-loaded-hook.md) — guard map API calls until `onLoad` / `useMapLoaded()`296. [map-debug-exposure.md](references/map-debug-exposure.md) — **`window.__mainMap` in dev/Playwright; `exposeMainMapForDebugging` in `onLoad`**307. [feature-state.md](references/feature-state.md) — **`setFeatureState` vs React `filter`; reset/diff; inspector sync**318. [flat-source-layer.md](references/flat-source-layer.md) — sibling `<Source>` + `<Layer>`, required props329. [layer-visibility-vs-unmount.md](references/layer-visibility-vs-unmount.md) — `layout.visibility` vs conditional render3310. [map-props-attribution-locale.md](references/map-props-attribution-locale.md) — `attributionControl={false}`, `locale`, `RTLTextPlugin`3411. [cursor-handling.md](references/cursor-handling.md) — `cursor` prop from hover state3512. [initial-view-state.md](references/initial-view-state.md) — uncontrolled `initialViewState` vs URL-driven vs bounds3613. [map-url-state.md](references/map-url-state.md) — Map ↔ URL sync (`onMoveEnd`); URL format → `tanstack-router-conventions` → `map-search-param.md`3714. [map-images-missing.md](references/map-images-missing.md) — `styleimagemissing` + dynamic icons3815. [map-images-proactive.md](references/map-images-proactive.md) — proactive `addImage` for known sprites (vzk-bw)3940Pair with: skill `tanstack-router-conventions` (`?map=` URL contract), skill `nuqs` (legacy URL parsers), skill `zustand-state-management` (map UI store like `useMapLoaded`), skill `playwright-skill` (E2E map helpers, `getMapLayerIds`).4142## Non-negotiable rules4344| Topic | Rule |45| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |46| Sources / layers | **`<Source>` + `<Layer>` in JSX only.** **Never** `map.addSource` / `map.addLayer` (or remove\*) for app data. Show/hide via `layout.visibility` or conditional render — see declarative-source-layer.md. |47| Map events | Camera, pointer, load, and tile lifecycle → `<Map>` callback props. **Not** `useEffect` + `map.on(…)` for the same behavior. |48| Map access | Wrap app map UI in `<MapProvider>`. Use `useMap()` keyed by `<Map id="…">`. **Never** pass `ref` to `<Map>` for child access. |49| Map ready | Guard `getStyle`, `queryRenderedFeatures`, `setFeatureState`, etc. with `useMapLoaded()` (set in `onLoad`). |50| Debug exposure | In `onLoad`, call `exposeMainMapForDebugging(event.target)` (dev + Playwright only). Agents/tests read `window.__mainMap` — see map-debug-exposure.md. |51| Layers | Flat siblings: one `<Source>` per source id, then `<Layer>` siblings (not nested layers under Source for refactorability). |52| Clicks | Put layer ids in `interactiveLayerIds`; read `event.features` in handlers — do not call `queryRenderedFeatures` for primary click picking unless syncing URL features. |53| Feature highlight | Inspector/form truth in React; map tint via `setFeatureState` (paint) or React `filter` layer — see feature-state.md. Clear old state before applying new. |54| Attribution | `attributionControl={false}` on `<Map>`; add `<AttributionControl compact>` as child. `compact` is not start-closed — see map-props-attribution-locale.md. |55| Viewport URL | Serialize `zoom/lat/lng` with zoom-aware lat/lng rounding; write on `onMoveEnd` with `history: 'replace'`. |56| View state | Prefer **uncontrolled** `initialViewState` seeded from URL; do not mirror full viewState in React state unless you need programmatic camera control. |5758## Quick import5960```tsx61import 'maplibre-gl/dist/maplibre-gl.css'62import { MapProvider, Map, Source, Layer, useMap } from 'react-map-gl/maplibre'63```