[Bug] reuseMaps: Mapbox.reuse() infinite-loops (tab freeze) when the recycled map's container is the mount container
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 8.5k
- Forks
- 1.4k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 3
Description
Description
With reuseMaps enabled, Mapbox.reuse() reparents the pooled map's DOM into the new mount container:
const oldContainer = map.getContainer();
container.className = oldContainer.className;
while (oldContainer.childNodes.length > 0) {
container.appendChild(oldContainer.childNodes[0]);
}
If container === oldContainer, appendChild moves the first child to the end of the same node, so childNodes.length never decreases and the loop never terminates. The main thread pins at 100% CPU and the tab is unrecoverable — DevTools cannot attach, and on weaker machines the whole browser goes down.
The same-container case is real, including in production builds: any environment that re-runs the <Map> mount effect while the DOM node survives triggers it. We hit two:
- Production: Next.js 16 App Router back/forward navigation restores the previous page while re-running effects (React Activity semantics). The mount effect's cleanup ran
recycle()when the user navigated away; on back-navigationreuse()pops that instance and mounts it into the very container it was parked with. First back-navigation to the map page froze the tab, every time, for every visitor. - Development: React
<StrictMode>effect replay reaches the same state when themapLibimport is already cached — effect run 1's.thencreates/reuses the map, the replay cleanup recycles it, and effect run 2 reuses it into the same still-mounted container. (A first-ever mount survives only because theimport('mapbox-gl')resolves too slowly for run 1 to finish.)
Verified by pausing the wedged VM over CDP: execution sits inside reuse() with container === oldContainer → true.
Two related consequences we found while fixing it:
- On the same-container path, the
[mapboxgl-children]div is lost:recycle()removed it, and sincereuse()returns the same instance,setMapInstance()bails out onObject.isand React never re-inserts it — any plain-JSX map child silently disappears after the reuse. Mapbox.savedMapsis a global LIFO with no map↔container association, so with two simultaneously pooled maps a reuse can pop the other map and append its DOM into a container that still holds the first map's canvas.
Environment
@vis.gl/react-mapbox8.1.2 (viareact-map-glv8); the unguarded loop is also present on currentmaster, and byte-identical copies ship in@vis.gl/react-maplibreand themapbox-legacyentrymapbox-gl3.28.1, React 19.2, Next.js 16.3- Reproduces deterministically on the first back-navigation to a page rendering
<Map reuseMaps>behind a Suspense boundary
Suggested fix
Treat container === oldContainer as "already in place", and move the [mapboxgl-children] cleanup out of recycle() into the different-container branch so the surviving React tree keeps its children div:
static reuse(props, container) {
const that = Mapbox.savedMaps.pop();
if (!that) return null;
const map = that.map;
const oldContainer = map.getContainer();
if (oldContainer !== container) {
container.className = oldContainer.className;
oldContainer.querySelector('[mapboxgl-children]')?.remove();
while (oldContainer.childNodes.length > 0) {
container.appendChild(oldContainer.childNodes[0]);
}
}
// ...
}
recycle() {
Mapbox.savedMaps.push(this);
}
We've been running exactly this as a pnpm patch in production since 2026-08-27 with no regressions (including a browser test that walks pin → popup → team page → back). Happy to send it as a PR.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the Mapbox.reuse() and recycle() entry points described in the issue, then compare their copies in react-maplibre and mapbox-legacy. Reproduce same-container reuse through StrictMode or navigation and run the browser regression path mentioned in the report; done means reuse no longer hangs and map children remain available afterward.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100