DEFAULT_SETTINGS.maxBounds in @vis.gl/react-maplibre has 360° longitude span, triggers maplibre-gl-js#6148 crash via _updateSettings fallback
Filing here since
visgl/react-maplibreis archived and points back to this monorepo. Bug is in the published@vis.gl/[email protected](also reached via[email protected]'s/maplibreentry).
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-maplibreCode reference
In the published @vis.gl/[email protected] (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:
- Firefox aggressively drops WebGL contexts when the tab is backgrounded.
- On tab return, maplibre's internal
_contextLostnullsthis.style, leaving the transform in a partial state. - A subsequent React render causes
setProps → _updateSettings. Under some prop-shape change (e.g.viewState/mapStyle/mapPropsreshape), themaxBoundskey in the previous-vs-current props comparison hits the asymmetric branch where it falls back toDEFAULT_SETTINGS.maxBounds. 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:
nullis 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'selsebranch (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/[email protected](via[email protected])[email protected]- Firefox (primary repro), Chrome (forced repro via
WEBGL_lose_context)
Related upstream: maplibre/maplibre-gl-js#6148 (open).
Source: visgl/react-map-gl