Environment
@rnmapbox/maps: 10.3.1
- React Native: 0.86.0 (New Architecture / Fabric — mandatory at this RN version)
- Expo SDK 57 (dev client), Android emulator (API 34, arm64)
- Mapbox implementation:
mapbox (v11 SDK)
Bug
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0 thrown from RNMBXSource.getChildAt (RNMBXSource.kt:211) via SurfaceMountingManager.removeViewAt, crashing the whole surface.
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.util.ArrayList.get(ArrayList.java:434)
at com.rnmapbox.rnmbx.components.styles.sources.RNMBXSource.getChildAt(RNMBXSource.kt:211)
at com.rnmapbox.rnmbx.components.styles.sources.RNMBXShapeSourceManager.getChildAt(RNMBXShapeSourceManager.kt:38)
at com.facebook.react.fabric.mounting.SurfaceMountingManager.removeViewAt(SurfaceMountingManager.kt:456)
at com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem.execute(IntBufferBatchMountItem.kt:125)
...
Reproduction
Conditionally mount/unmount a ShapeSource + its first child Layer together in a single React commit, on a screen with multiple ShapeSources:
{shape ? (
<ShapeSource id="fog" shape={shape}>
<FillLayer id="fog-fill" style={{ fillColor: "#C9C4D4" }} />
</ShapeSource>
) : null}
When shape flips between null and non-null (or the screen with several such pairs unmounts in one commit), Fabric's view hierarchy and RNMBXSource.mSubFeatures desync: Fabric issues removeViewAt/getChildAt for child indices the source's own bookkeeping no longer has. Also reproduced with Index 1 out of bounds for length 0 during multi-layer screen teardown.
Root cause (from reading RNMBXSource.kt)
mSubFeatures is mutated by addLayer/removeLayer using the child positions Fabric passes in, but getChildAt/removeLayer index into it without bounds checks. During batched mount/unmount (IntBufferBatchMountItem) the two child models can briefly disagree, and the unchecked ArrayList.get turns a transient desync into a fatal crash of the entire react surface.
Workaround (app side)
Never conditionally mount the pair; always render ShapeSource + Layer and swap an empty featureCollection([]) in/out via the shape prop only.
Suggested fix
Bounds-check the three touch points (clamp insert position in addLayer, no-op out-of-range removeLayer, safe fallback in getChildAt). Patch we're shipping via patch-package:
diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXSource.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXSource.kt
index 3dec5bb..c87ed36 100644
--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXSource.kt
+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXSource.kt
@@ -189,10 +189,21 @@ abstract class RNMBXSource<T : Source?>(context: Context?) : AbstractMapFeature(
} else {
false
}
- mSubFeatures.add(childPosition, FeatureInfo(feature, added))
+ // Fabric's shadow tree and this class's own mSubFeatures bookkeeping can
+ // momentarily disagree on child count during rapid mount/unmount batches
+ // (observed on Android when a MapView with multiple ShapeSource/Layer
+ // pairs tears down in a single commit). Clamping here avoids an
+ // IndexOutOfBoundsException from an out-of-range insert index rather
+ // than crashing the whole surface.
+ val safePosition = childPosition.coerceIn(0, mSubFeatures.size)
+ mSubFeatures.add(safePosition, FeatureInfo(feature, added))
}
fun removeLayer(childPosition: Int) {
+ if (childPosition < 0 || childPosition >= mSubFeatures.size) {
+ Logger.w(LOG_TAG, "removeLayer: index $childPosition out of bounds for size ${mSubFeatures.size} on source $iD, ignoring")
+ return
+ }
var featureInfo = mSubFeatures[childPosition]
if (featureInfo.added) {
val mapView = mMapView
@@ -208,7 +219,9 @@ abstract class RNMBXSource<T : Source?>(context: Context?) : AbstractMapFeature(
get() = mSubFeatures.map { it.feature }.filterNotNull()
override fun getChildAt(childPosition: Int): View {
- return childViews[childPosition]
+ childViews.getOrNull(childPosition)?.let { return it }
+ Logger.w(LOG_TAG, "getChildAt: index $childPosition out of bounds for size ${childViews.size} on source $iD, returning placeholder")
+ return View(context)
}
override fun getChildCount(): Int {
Environment
@rnmapbox/maps: 10.3.1mapbox(v11 SDK)Bug
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0thrown fromRNMBXSource.getChildAt(RNMBXSource.kt:211) viaSurfaceMountingManager.removeViewAt, crashing the whole surface.Reproduction
Conditionally mount/unmount a
ShapeSource+ its first childLayertogether in a single React commit, on a screen with multiple ShapeSources:When
shapeflips between null and non-null (or the screen with several such pairs unmounts in one commit), Fabric's view hierarchy andRNMBXSource.mSubFeaturesdesync: Fabric issuesremoveViewAt/getChildAtfor child indices the source's own bookkeeping no longer has. Also reproduced withIndex 1 out of bounds for length 0during multi-layer screen teardown.Root cause (from reading RNMBXSource.kt)
mSubFeaturesis mutated byaddLayer/removeLayerusing the child positions Fabric passes in, butgetChildAt/removeLayerindex into it without bounds checks. During batched mount/unmount (IntBufferBatchMountItem) the two child models can briefly disagree, and the uncheckedArrayList.getturns a transient desync into a fatal crash of the entire react surface.Workaround (app side)
Never conditionally mount the pair; always render
ShapeSource+Layerand swap an emptyfeatureCollection([])in/out via theshapeprop only.Suggested fix
Bounds-check the three touch points (clamp insert position in
addLayer, no-op out-of-rangeremoveLayer, safe fallback ingetChildAt). Patch we're shipping via patch-package: