Skip to content

DEFAULT_SETTINGS.maxBounds in @vis.gl/react-maplibre has 360° longitude span, triggers maplibre-gl-js#6148 crash via _updateSettings fallback #2591

Description

@4nrry

Filing here since visgl/react-maplibre is archived and points back to this monorepo. Bug is in the published @vis.gl/react-maplibre@8.1.1 (also reached via react-map-gl@8.1.1's /maplibre entry).

Summary

DEFAULT_SETTINGS.maxBounds is hard-coded to [-180, -85.051129, 180, 85.051129], which has a longitude span of exactly 360°. When _updateSettings falls back to this default (when the maxBounds key is present in only one of nextProps / currProps), it calls map.setMaxBounds([-180, -85.051129, 180, 85.051129]) on the underlying maplibre instance. Because the longitude bounds span the full world, the upstream bug maplibre/maplibre-gl-js#6148 fires: the projection matrix becomes singular and non-invertible, throwing:

TypeError: Cannot read properties of null (reading '0')
(Firefox: can't access property 0, i is null)

Stack

Ge                  maplibre-gl-...js
_calcMatrices
calcMatrices
_calcMatrices
setZoom
constrainInternal
setMaxBounds        // TransformHelper
setMaxBounds        // MercatorTransformImpl
setMaxBounds        // Map (maplibre)
_updateSettings     // @vis.gl/react-maplibre

Code reference

In the published @vis.gl/react-maplibre@8.1.1 (dist/index.cjs lines 220-228, equivalent in source maplibre/maplibre.ts):

const DEFAULT_SETTINGS = {
  minZoom: 0,
  maxZoom: 22,
  minPitch: 0,
  maxPitch: 85,
  maxBounds: [-180, -85.051129, 180, 85.051129], // <-- 360° longitude span
  projection: 'mercator',
  renderWorldCopies: true,
};

And the fallback in _updateSettings (dist/index.cjs lines 517-529):

_updateSettings(nextProps, currProps) {
  const map = this._map;
  let changed = false;
  for (const propName of settingNames) {
    const propPresent = propName in nextProps || propName in currProps;
    if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) {
      changed = true;
      const nextValue = propName in nextProps
        ? nextProps[propName]
        : DEFAULT_SETTINGS[propName];
      // ^ when 'maxBounds' is in currProps but not nextProps, this picks
      //   the 360°-span world bounds, which maplibre then chokes on.
      const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];
      setter?.call(map, nextValue);
    }
  }
  return changed;
}

How we hit it in production

Consumer code never passes maxBounds to <Map>. But:

  1. Firefox aggressively drops WebGL contexts when the tab is backgrounded.
  2. On tab return, maplibre's internal _contextLost nulls this.style, leaving the transform in a partial state.
  3. A subsequent React render causes setProps → _updateSettings. Under some prop-shape change (e.g. viewState / mapStyle / mapProps reshape), the maxBounds key in the previous-vs-current props comparison hits the asymmetric branch where it falls back to DEFAULT_SETTINGS.maxBounds.
  4. setMaxBounds([-180, -85.05, 180, 85.05]) runs against the partially-restored transform → maplibre tries to invert a singular matrix → null deref.

Reproduces on both Firefox and Chrome. We verified by forcing context loss with WEBGL_lose_context.loseContext() followed by restoreContext() — Chrome hits the same crash, just less spontaneously than Firefox.

Suggested fix

Change DEFAULT_SETTINGS.maxBounds from the 360° world bounds to null:

 const DEFAULT_SETTINGS = {
   minZoom: 0,
   maxZoom: 22,
   minPitch: 0,
   maxPitch: 85,
-  maxBounds: [-180, -85.051129, 180, 85.051129],
+  maxBounds: null,
   projection: 'mercator',
   renderWorldCopies: true,
 };

Rationale:

  • null is the natural identity for "prop was omitted, don't constrain" — matches maplibre-gl-js's own default (no bounds).
  • map.setMaxBounds(null) is a no-op in maplibre's else branch (just clears _lngRange / _latRange), so it can't trigger the singular-matrix path.
  • It preserves the loop's correctness for the other settings, which are simple numeric / string defaults.

Alternative if maintainers want to preserve the existing intent: bounds strictly inside (-180, 180) longitude span (e.g. [-179.999, -85.051129, 179.999, 85.051129]) as upstream maplibre-gl-js#6148 suggests as a workaround. But null is cleaner.

Workaround for current downstream consumers

Always pass an explicit, stable value to <Map> so the prop never drops out of nextProps:

<Map
  {...viewState}
  maxBounds={null as never} // stable null short-circuits the DEFAULT_SETTINGS fallback
  mapStyle={...}
/>

deepEqual(null, null) === true keeps the loop from entering the setter branch.

Environment

  • @vis.gl/react-maplibre@8.1.1 (via react-map-gl@8.1.1)
  • maplibre-gl@5.23.0
  • Firefox (primary repro), Chrome (forced repro via WEBGL_lose_context)

Related upstream: maplibre/maplibre-gl-js#6148 (open).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions