visgl / visgl/react-map-gl

[Bug] reuseMaps: Mapbox.reuse() infinite-loops (tab freeze) when the recycled map's container is the mount container

Open
#2,613 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. 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-navigation reuse() 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.
  2. Development: React <StrictMode> effect replay reaches the same state when the mapLib import is already cached — effect run 1's .then creates/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 the import('mapbox-gl') resolves too slowly for run 1 to finish.)

Verified by pausing the wedged VM over CDP: execution sits inside reuse() with container === oldContainertrue.

Two related consequences we found while fixing it:

  • On the same-container path, the [mapboxgl-children] div is lost: recycle() removed it, and since reuse() returns the same instance, setMapInstance() bails out on Object.is and React never re-inserts it — any plain-JSX map child silently disappears after the reuse.
  • Mapbox.savedMaps is 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-mapbox 8.1.2 (via react-map-gl v8); the unguarded loop is also present on current master, and byte-identical copies ship in @vis.gl/react-maplibre and the mapbox-legacy entry
  • mapbox-gl 3.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.