Skip to content

Unmount the canvas-in-layer root when the map is removed - #172

Open
luiskcs89 wants to merge 1 commit into
RodrigoHamuy:mainfrom
luiskcs89:fix/unmount-root-on-map-remove
Open

luiskcs89 wants to merge 1 commit into
RodrigoHamuy:mainfrom
luiskcs89:fix/unmount-root-on-map-remove

Conversation

@luiskcs89

Copy link
Copy Markdown

Fixes #171

Problem

<Canvas overlay={false}> only unmounts its R3F root in the custom layer onRemove. map.remove() does not call onRemove (Mapbox and MapLibre only call it from removeLayer). react-map-gl's <Layer> also skips removeLayer once the map is gone. So when the whole <Map> unmounts, the root stays in _roots and no cleanup runs inside the scene.

Fix

The layer now also has an onAdd. It listens to the map remove event and unmounts the root then. onRemove stops listening and unmounts the root, as before.

// map.remove() does not call the layer onRemove
const onAdd = useFunction(() => {
  map.on('remove', onRemove);
})

const onRemove = useFunction(() => {
  map.off('remove', onRemove);
  root.unmount();
})

Why this way:

  • The listener follows the map's own layer life (onAdd / onRemove), not React effects. So it does not depend on the order of effect cleanups between <Map> and <Canvas>, and StrictMode only pairs an extra off/on.
  • root.unmount() uses the canvas that createRoot got at creation. Mapbox sets map._canvas to undefined before it fires remove, so map.getCanvas() must not be read there.
  • Both Mapbox and MapLibre call custom layer onAdd from Style.addLayer, and fire remove at the end of map.remove().

MapEventType gets a remove entry for the types.

Testing

A repro app (React 19 StrictMode, MapLibre, R3F 9.6.1, react-map-gl 8.1.1) toggles the whole <Map> 3 times, then only the <Canvas>. It reads _roots.size, counts tunnel-rat overlay nodes and clicks the mesh after each mount.

step before: roots / overlays after: roots / overlays
mounted 1 / 1 1 / 1
map unmounted 1 / 1 0 / 0
map mounted again 2 / 2 1 / 1
after 3 cycles, mounted 4 / 4 1 / 1
only <Canvas> unmounted (prod build) 3 / 3 0 / 0

Mesh clicks work after each map mount, in dev and in a production build.

Tested with MapLibre only. Mapbox was checked by reading the mapbox-gl 3.24.0 sources.

Not in this PR

In dev StrictMode, if <Canvas> mounts on a map whose style is already loaded, react-map-gl's <Layer> calls removeLayer in StrictMode's test cleanup. That calls onRemove and unmounts the root, and the layer is added back with a dead root (events stop working). This happens before and after this PR. A fix probably needs the root to be created again after an unmount, which R3F makes hard because it removes _roots entries 500 ms later.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 88838d9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
react-three-map Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Title: <Canvas overlay={false}> leaks its R3F root when the whole map unmounts

1 participant