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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions