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:
- Firefox aggressively drops WebGL contexts when the tab is backgrounded.
- On tab return, maplibre's internal
_contextLost nulls this.style, leaving the transform in a partial state.
- 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.
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).
Summary
DEFAULT_SETTINGS.maxBoundsis hard-coded to[-180, -85.051129, 180, 85.051129], which has a longitude span of exactly 360°. When_updateSettingsfalls back to this default (when themaxBoundskey is present in only one ofnextProps/currProps), it callsmap.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:Stack
Code reference
In the published
@vis.gl/react-maplibre@8.1.1(dist/index.cjslines 220-228, equivalent in sourcemaplibre/maplibre.ts):And the fallback in
_updateSettings(dist/index.cjslines 517-529):How we hit it in production
Consumer code never passes
maxBoundsto<Map>. But:_contextLostnullsthis.style, leaving the transform in a partial state.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 byrestoreContext()— Chrome hits the same crash, just less spontaneously than Firefox.Suggested fix
Change
DEFAULT_SETTINGS.maxBoundsfrom the 360° world bounds tonull: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.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. Butnullis cleaner.Workaround for current downstream consumers
Always pass an explicit, stable value to
<Map>so the prop never drops out ofnextProps:deepEqual(null, null) === truekeeps the loop from entering the setter branch.Environment
@vis.gl/react-maplibre@8.1.1(viareact-map-gl@8.1.1)maplibre-gl@5.23.0WEBGL_lose_context)Related upstream: maplibre/maplibre-gl-js#6148 (open).