Upgrade @maplibre/maplibre-react-native from 10 to 11

v11 is a breaking API rework (renamed components, restructured props,
event payloads on nativeEvent) and requires React Native's new
architecture. Doing it as a manual migration instead of taking
dependabot's #268 because the renames need code changes.

Changes in apps/mobile:

- RouteMap.tsx — switch to named imports; rename MapView→Map,
  ShapeSource→GeoJSONSource (with shape→data), LineLayer→Layer
  (with type="line" and paint instead of style), MarkerView→
  ViewAnnotation (with coordinate→lngLat); Camera uses
  initialViewState with bounds as [w,s,e,n] and a separate padding
  object; onLongPress reads lngLat from event.nativeEvent.

- app.config.ts — set `newArchEnabled: true`. MapLibre RN v11
  supports only the new architecture, so Fabric/TurboModules must
  be on for the Expo prebuild. Cast the config type since Expo SDK
  55's ExpoConfig typings don't yet include the field (the runtime
  does).

- package.json — `^10.4.2` → `^11.0.0`.

Supersedes dependabot #268; it'll auto-close once this merges.

Runtime verification requires an EAS dev build (native deps
changed + new arch flipped). JS-only surface typechecks + lints +
unit tests clean locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-19 12:09:06 +02:00
parent c3d9514f4d
commit 7e302b1fbe
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
4 changed files with 104 additions and 53 deletions

View file

@ -1,11 +1,16 @@
import { ExpoConfig, ConfigContext } from "expo/config";
export default ({ config }: ConfigContext): ExpoConfig => ({
// `newArchEnabled` isn't in the Expo SDK 55 typings yet but is an
// accepted runtime flag. MapLibre RN v11 requires the new architecture.
type ExpoConfigWithNewArch = ExpoConfig & { newArchEnabled?: boolean };
export default ({ config }: ConfigContext): ExpoConfigWithNewArch => ({
...config,
name: "trails.cool",
slug: "mobile",
version: "0.0.1",
scheme: "trailscool",
newArchEnabled: true,
orientation: "portrait",
icon: "./assets/icon.png",
userInterfaceStyle: "light",

View file

@ -1,6 +1,13 @@
import { useRef, useCallback } from "react";
import { StyleSheet, View, Text } from "react-native";
import MapLibreGL from "@maplibre/maplibre-react-native";
import {
Camera,
GeoJSONSource,
Layer,
Map,
ViewAnnotation,
type CameraRef,
} from "@maplibre/maplibre-react-native";
import type { Waypoint } from "@trails-cool/types";
import type { RouteSegment } from "./use-route-editor";
@ -40,7 +47,10 @@ export function RouteMap({
onWaypointDragEnd: _onWaypointDragEnd,
onWaypointPress,
}: RouteMapProps) {
if (!MapLibreGL) {
// `Map` is undefined when the native module isn't linked (e.g. running
// in Expo Go). Same safety net as before the v11 upgrade, just against
// a named import instead of the removed default export.
if (!Map) {
return (
<View style={styles.fallback}>
<Text style={styles.fallbackText}>Map</Text>
@ -64,15 +74,16 @@ function RouteMapInner({
onWaypointDragEnd: _onWaypointDragEnd,
onWaypointPress,
}: RouteMapProps) {
const ML = MapLibreGL!;
const cameraRef = useRef<MapLibreGL.CameraRef>(null);
const cameraRef = useRef<CameraRef>(null);
const handleLongPress = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(event: any) => {
const coords = event?.geometry?.coordinates;
if (Array.isArray(coords) && coords.length >= 2) {
onLongPress(coords[1] as number, coords[0] as number);
// v11 event shape: payload lives on `event.nativeEvent.lngLat`
// (the old `event.geometry.coordinates` is gone).
const lngLat = event?.nativeEvent?.lngLat;
if (Array.isArray(lngLat) && lngLat.length >= 2) {
onLongPress(lngLat[1] as number, lngLat[0] as number);
}
},
[onLongPress],
@ -96,64 +107,59 @@ function RouteMapInner({
return (
<View style={styles.container}>
<ML.MapView
<Map
style={styles.map}
mapStyle={OSM_STYLE}
onLongPress={handleLongPress}
attributionEnabled={false}
logoEnabled={false}
attribution={false}
logo={false}
>
{bounds && (
<ML.Camera
<Camera
ref={cameraRef}
defaultSettings={{
bounds: {
ne: bounds.ne,
sw: bounds.sw,
paddingTop: 40,
paddingBottom: 40,
paddingLeft: 40,
paddingRight: 40,
},
initialViewState={{
bounds: [bounds.sw[0], bounds.sw[1], bounds.ne[0], bounds.ne[1]],
padding: { top: 40, right: 40, bottom: 40, left: 40 },
}}
/>
)}
{!bounds && (
<ML.Camera
defaultSettings={{
centerCoordinate: [10.0, 50.1],
zoomLevel: 6,
<Camera
initialViewState={{
center: [10.0, 50.1],
zoom: 6,
}}
/>
)}
{/* Route line */}
<ML.ShapeSource id="route" shape={routeGeojson}>
<ML.LineLayer
<GeoJSONSource id="route" data={routeGeojson}>
<Layer
id="route-line"
style={{
lineColor: "#2563eb",
lineWidth: 4,
lineOpacity: computing ? 0.4 : 1,
type="line"
paint={{
"line-color": "#2563eb",
"line-width": 4,
"line-opacity": computing ? 0.4 : 1,
}}
/>
</ML.ShapeSource>
</GeoJSONSource>
{/* Waypoint markers */}
{waypoints.map((wp, i) => (
<ML.MarkerView
<ViewAnnotation
key={`wp-${i}`}
coordinate={[wp.lon, wp.lat]}
lngLat={[wp.lon, wp.lat]}
>
<WaypointMarker
index={i}
isDayBreak={wp.isDayBreak}
onPress={() => onWaypointPress(i)}
/>
</ML.MarkerView>
</ViewAnnotation>
))}
</ML.MapView>
</Map>
{computing && (
<View style={styles.computingBanner}>

View file

@ -31,7 +31,7 @@
"dependencies": {
"@expo/metro-runtime": "^55.0.9",
"@gorhom/bottom-sheet": "^5.2.9",
"@maplibre/maplibre-react-native": "^10.4.2",
"@maplibre/maplibre-react-native": "^11.0.0",
"@sentry/cli": "^3.3.5",
"@sentry/react-native": "~7.11.0",
"@trails-cool/api": "workspace:*",