[Bug]: Android: initial camera from Camera defaultSettings lands at latitude 0 on a cold style cache
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 2.9k
- Forks
- 947
- Avg merge
- 6d 37m
- Merged PRs (30d)
- 1
Description
Follow-up to #3818 as requested there. #3818 is closed by #4040, but a related failure is still present on 10.3.5: the initial camera keeps the longitude and ends up at latitude 0 whenever the Mapbox style cache is cold.
Reproducer, evidence and the patch I am running: https://github.com/krisgerhard/rnmapbox-bounds-initial-camera-repro
Mapbox Version
11.24.2
React Native Version
0.86.2
Platform
Android
@rnmapbox/maps version
10.3.5
Standalone component to reproduce
BugReportExample.js
import React, { useRef, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Camera, MapView } from '@rnmapbox/maps';
const BOUNDS = { ne: [137.9, 35.6], sw: [136.8, 34.8] };
const EXPECTED = [
(BOUNDS.ne[0] + BOUNDS.sw[0]) / 2,
(BOUNDS.ne[1] + BOUNDS.sw[1]) / 2,
];
export default function BugReportExample() {
const mountedAt = useRef(Date.now());
const [center, setCenter] = useState(null);
const [log, setLog] = useState([]);
const push = (label) => {
const entry = `+${Date.now() - mountedAt.current}ms ${label}`;
console.log(`[repro] ${entry}`);
setLog((prev) => (prev.length >= 8 ? prev : [...prev, entry]));
};
const failed =
center !== null &&
(Math.abs(center[0] - EXPECTED[0]) > 0.5 || Math.abs(center[1] - EXPECTED[1]) > 0.5);
return (
<View style={styles.root}>
<MapView
style={styles.map}
onDidFinishLoadingStyle={() => push('onDidFinishLoadingStyle')}
onDidFinishLoadingMap={() => push('onDidFinishLoadingMap')}
onMapIdle={(state) => {
setCenter(state.properties.center);
push(
`onMapIdle center=[${state.properties.center
.map((n) => n.toFixed(3))
.join(', ')}]`,
);
}}
>
<Camera defaultSettings={{ bounds: BOUNDS }} />
</MapView>
<View style={styles.overlay}>
<Text style={styles.line}>
expected center [{EXPECTED.map((n) => n.toFixed(3)).join(', ')}]
</Text>
<Text style={styles.line}>
actual center{' '}
{center === null ? '—' : `[${center.map((n) => n.toFixed(3)).join(', ')}]`}
</Text>
{center !== null && (
<Text style={failed ? styles.fail : styles.pass}>{failed ? 'FAIL' : 'PASS'}</Text>
)}
{log.map((line, i) => (
<Text key={i} style={styles.logLine}>
{line}
</Text>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
map: { flex: 1 },
overlay: {
position: 'absolute',
top: 12,
left: 12,
right: 12,
padding: 12,
borderRadius: 8,
backgroundColor: 'rgba(0, 0, 0, 0.78)',
},
line: { color: '#ddd', fontSize: 12 },
pass: { color: '#5cff9d', fontWeight: '700', marginVertical: 4 },
fail: { color: '#ff6b6b', fontWeight: '700', marginVertical: 4 },
logLine: { color: '#bbb', fontSize: 11 },
});
Observed behavior and steps to reproduce
The camera lands at [137.350, 0.000] instead of [137.350, 35.200] — right longitude, latitude exactly 0, so the map opens on the ocean.
- Install the app fresh (
adb uninstall <pkg>first — this is what clears the Mapbox style cache). - Launch it. The
Camerahas no props other thandefaultSettings, so after #4040 no empty stop is sent (nativeStop === null). - Read the center from
onMapIdle/ the overlay.
Only the first launch after an install is wrong; every later launch of the same install is correct, which is why this looks flaky in the wild.
| build | defaultSettings |
fresh installs | landed at latitude 0 |
|---|---|---|---|
| 10.3.5 | { bounds } |
4 | 4 |
| 10.3.5 + patch below | { bounds } |
4 | 0 |
| 10.3.5 | { centerCoordinate, zoomLevel } |
8 | 6 |
| 10.3.5 + patch below | { centerCoordinate, zoomLevel } |
4 | 0 |
| 10.3.5, warm relaunch (no reinstall) | { bounds } |
1 | 0 |
So bounds fails every time and centerCoordinate fails most of the time — #4213 (onCameraChanged firing [0, 0] / [lng, 0] on launch) looks like the same root cause seen through the event stream.
Reproduced on a physical OnePlus NE2213 (Android 16, API 36) and on the Medium_Phone_API_36.0 AVD, both New Architecture, debug builds.
Unpatched (emulator, fresh install) vs. patched:

I ReactNativeJS: [repro +681ms] onDidFinishLoadingStyle
I ReactNativeJS: [repro +730ms] onDidFinishLoadingMap
I ReactNativeJS: [repro +1018ms] onMapIdle center=[137.350, 0.000] zoom=7.85
Expected behavior
<Camera defaultSettings={{ bounds }} /> should put the initial camera inside the requested bounds on the first launch too, as it does on every subsequent launch.
Notes / preliminary analysis
RNMBXCamera declares requiresStyleLoad = false, so RNMBXMapView.addFeature adds it immediately and addToMap() → setInitialCamera() runs before the style is loaded. setInitialCamera applies the stop through a zero-duration flyTo (CameraUpdateItem.run()); applied that early the latitude does not stick while the longitude does. This matches @Elter71's observation in #3818 that "during the first flyTo method call, Mapbox doesn't center the map on the latitude".
Instrumenting RNMBXCamera in our production app (with Log.e — Logger.w is level-gated below WARN and silently dropped the output) showed:
- the map view is already fully laid out (
1080×2412) — not a layout-size race; CameraStop.cameraForCoordinateBounds(...)returns the correct camera (center = 35.205, 137.357,zoom = 8.87);setInitialCameraruns ~270 ms beforeonDidFinishLoadingMapon a cold start; on a warm start the cached style is loaded before the camera is added, which is exactly the difference between a failing and a passing launch.
Deferring the default stop until the style is loaded fixes it in all configurations I measured. getStyle invokes the callback immediately when the style is already loaded, so the warm path is unchanged and only the cold-start initial camera is delayed by one style load (~150–200 ms):
private fun setInitialCamera(mapView: RNMBXMapView) {
- mDefaultStop?.let {
- val map = mapView.getMapboxMap()
-
- it.setDuration(0)
- it.setMode(CameraMode.NONE)
- val item = it.toCameraUpdate(mapView)
- item.run()
- }
+ val defaultStop = mDefaultStop ?: return
+ mapView.getMapboxMap().getStyle(object : Style.OnStyleLoaded {
+ override fun onStyleLoaded(style: Style) {
+ defaultStop.setDuration(0)
+ defaultStop.setMode(CameraMode.NONE)
+ defaultStop.toCameraUpdate(mapView).run()
+ }
+ })
}
We have been running this patch in production for a while with no side effects. I don't know whether the deferral or requiresStyleLoad = true is the direction you'd prefer (the latter looked riskier to me).
Additional links and references
- Reproducer with results, screenshots and the patch: https://github.com/krisgerhard/rnmapbox-bounds-initial-camera-repro
- #3818 — original report, closed by #4040 (which fixed the empty-stop-clobbers-
defaultStoppart) - #3449 — duplicate of #3818
- #4213 —
onCameraChangedfiring[0, 0]on launch, likely the same root cause
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 with RNMBXCamera.setInitialCamera and CameraUpdateItem.run(), then run the linked reproducer after clearing the Android app data to exercise the cold style-cache path. Done means the initial camera reaches the requested bounds, including the expected latitude, on the first launch while warm launches remain correct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, react-native
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100