DEFAULT_SETTINGS.maxBounds in @vis.gl/react-maplibre has 360° longitude span, triggers maplibre-gl-js#6148 crash via _updateSettings fallback
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 8.5k
- Forks
- 1.4k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 3
Description
Filing here since
visgl/react-maplibreis archived and points back to this monorepo. Bug is in the published@vis.gl/react-maplibre@8.1.1(also reached viareact-map-gl@8.1.1'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-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
_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/react-maplibre@8.1.1(viareact-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).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in maplibre/maplibre.ts, reading DEFAULT_SETTINGS and _updateSettings to understand the asymmetric fallback for maxBounds. Verify the change against the reported context-loss reproduction and confirm that omitted maxBounds no longer sends a 360° longitude span or triggers the maplibre crash.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100