@yoyomobility/expo-yandex-maps
v1.2.2
Published
Yandex Maps SDK for Expo
Readme
expo-yandex-maps
Yandex Maps SDK for Expo
API documentation
Traffic-aware routes (v1.1.0+)
Three primitives for traffic-aware route rendering. Color mapping stays on the app side — the library never hardcodes colors.
Per-segment polyline colors
<PolylineView
points={section.points}
strokeColor="#808080" // fallback when strokeColors is not set
strokeColors={segmentColors} // length MUST be points.length - 1
gradientLength={15} // meters of smooth transition between segments, default 0 (hard edges)
/>strokeColors overrides strokeColor when set. On a length mismatch the
library warns in dev and falls back to the scalar strokeColor.
Jam data in findRoutes
Driving sections (vehicles: ['car']) include a jams array with one
JamType ('unknown' | 'blocked' | 'free' | 'light' | 'hard' | 'veryHard')
per segment: jams.length === points.length - 1, aligned with the returned
(simplified) geometry — segments merged by simplification aggregate worst-wins.
The field is omitted when MapKit returns no jam data (offline routing, regions
without coverage). jams is a snapshot taken when the route was built —
refetch the route to refresh it.
const { routes } = await map.current.findRoutes(points, ["car"], {});
const section = routes[0].sections[0];
// section.jams?.[i] describes the segment between points[i] and points[i + 1]
section.jams?.map((jam) => myTheme.jamColors[jam]); // → strokeColorssimplificationPreset is now honored on both platforms (previously iOS
ignored it and returned the full geometry), so iOS and Android produce the
same segments.
Traffic level without the tiles overlay
await map.current.setTrafficDataEnabled(true); // subscribe to onTrafficChanged, no tiles
await map.current.setTrafficVisible(true); // tiles overlay, independent of the above
// onTrafficChanged={(e) => e.nativeEvent}
// { status: 'changed' | 'loading' | 'expired',
// data?: { level: number /* 0..10 */, color: 'red' | 'yellow' | 'green' } }data.level describes the congestion of the visible map region (like the
badge in Yandex Maps), not of a route. setTrafficVisible and
setTrafficDataEnabled are independent: disabling one never drops the
subscription held by the other, and repeated calls never duplicate it.
setTrafficStyle(style: string): Promise<boolean> applies a MapKit JSON style
to the traffic tiles, e.g. [{"stylers": {"opacity": 0.35, "saturation": -0.7}}].
Geocoder error handling (v1.1.0+)
geocode / reverseGeocode no longer hang silently: failed HTTP requests
reject with GeocodingApiError carrying the HTTP status and the parsed
Yandex body (yandexResponse), e.g.
Geocoding request failed with status 403: Invalid api key. Requests time out
after GeocodingApi.REQUEST_TIMEOUT_MS (15 s by default, status: 0), and a
missing geocodeKey fails fast with a descriptive message.
import { GeocodingApiError } from "expo-yandex-maps";
try {
const address = await map.geocode(point);
} catch (e) {
if (e instanceof GeocodingApiError) console.warn(e.status, e.message);
}Note: the HTTP Geocoder needs its own key of type "JavaScript API and HTTP Geocoder" from the Yandex Developer console — a MapKit SDK key returns 403.
Camera and marker results (v1.2.0+)
Every camera and marker animation promise now resolves with the outcome of the
movement instead of undefined (additive — callers that ignore the result are
unaffected):
const { completed, position } = await map.setCameraPosition(target, 0.5, 0);
// completed: false — the flight was interrupted (user gesture or the next
// programmatic move); position — the camera at resolve time.
const fit = await map.fitMarkers(routePoints);
// FitMarkersResult extends CameraMoveResult with an atomic measurement taken
// inside the native callback (no bridge race between getScreenRect and
// getScreenPoints):
// fit.focusRect — the focus rect the content was fitted into (physical px)
// fit.screenBounds — screen bbox of the fitted points, or null when a bbox
// corner is not projectable (tilt + interrupted flight)
const res = await map.focusRect(rect, 0.4, { fitVisible: true });
// res.completed — false when superseded by the next focusRect call
// res.appliedRect — the rect actually applied after clamping to the window;
// null — the rect was dropped as degenerate
const move = await marker.animatedMoveTo(point, 500); // { completed, point }
const turn = await marker.animatedRotateTo(90, 300); // { completed, angle }
// completed: false — superseded by the next command or the marker unmounted;
// never a rejection.Camera movement lifecycle events
onCameraPositionChangeStart fires once at the beginning of a logical camera
movement, onCameraPositionChangeEnd once at its end. Per-frame moves produced
by the declarative focusRect stream (Reanimated + bottom sheet) coalesce
natively: intermediate ends are debounced ~200 ms and collapse into a single
start/end pair per drag, so consumers like "geocode on camera stop" or
isMapMoving flags don't fire 60 times a second. onCameraPositionChange
stays per-frame and, since 1.2.0, fires for every native camera callback on
both platforms, finished: true ones included (previously iOS skipped change
for finished callbacks, which made duration-0 moves invisible to it) — filter
by finished if you only want intermediate frames. Gestures and ordinary
programmatic moves are not affected. If a new movement starts while a
coalesced end is still pending, that end flushes immediately, so it never
fires mid-movement with a stale position.
Camera events carry reason: "GESTURES", "APPLICATION", or
"FOCUS_RECT" for moves produced by the declarative focusRect stream —
uniform across platforms since 1.2.0 (migration note: iOS previously sent
lowercase "gestures"/"application"). Filter on it when a "map is moving"
indicator should ignore the sheet dragging the rect:
onCameraPositionChange={(e) => {
if (e.nativeEvent.reason !== "FOCUS_RECT") setIsMapMoving(true);
}}Resetting focusRect
Setting the focusRect prop to null (or removing it) now resets the native
focus rect entirely — previously null was ignored and the only "reset" was
sending a full-window rect. The reset deliberately skips the fitVisible zoom
compensation: it releases the camera, it does not reframe.
initialRegion is applied once
initialRegion moves the camera only on the first application and ignores
re-application — expo-modules re-applies the whole props bag on every view
update, which used to snap the camera back to the initial region on unrelated
prop changes (and produced per-frame Cannot set prop 'initialRegion'
conversion errors on Android after the prop was removed from JSX). Workarounds
that removed the prop after the map loaded are no longer needed.
Prop application semantics (v1.2.1+)
On the New Architecture expo-modules re-applies the whole props bag on
every view update — during a focusRect stream from a bottom sheet or any
JS-driven animation inside the map subtree that is every frame. Two rules make
this safe:
- Re-applying the same value is a native no-op. Every heavy prop handler
(
mapStyle,userLocationIcon*,showUserPosition,followUser,nightMode,mapType, allMarkerView/PolylineView/PolygonView/CircleViewprops) compares against the applied value and skips repeats. Before 1.2.1mapStylerestyled the tiles per frame (visible flicker + gesture stutter) whenever anything in the map subtree animated, and a route polyline rebuilt its geometry and per-segment color palette per frame. Real changes pass through, so ReanimatedanimatedPropson any of these components keep working — each animation frame carries a new value. - Removing a prop from JSX keeps the last applied value. A removed prop
stays in the Fabric props bag as
nullforever; all handlers ignorenullinstead of throwing per-pass conversion errors. Conditional passing ({...(cond && { mapStyle })}) is legal. To reset something, pass an explicit value — the exceptions with real null semantics arefocusRect(nullresets the native focus rect) andinitialRegion(apply-once). Props declared with a native default (scrollGesturesEnabledand the other gesture toggles) fall back to that default when removed instead.
Also since 1.2.1: showUserPosition={false} / followUser={false} no longer
create the user-location layer, and out-of-order icon downloads
(userLocationIcon, marker source) can't overwrite a newer request.
Marker and camera improvements (v1.1.0+)
MarkerView: declarativedirectionprop (placemark azimuth in degrees, use withrotated) andonMarkerReadyevent fired when the native placemark is attached and imperative commands are safe to call.animatedMoveTo/animatedRotateToand camera methods (setCameraPosition,setCenter,setZoom,fitMarkers,fitAllMarkers) resolve their promises when the animation actually finishes, not when it starts.- Marker animation commands no longer produce unhandled promise rejections when the marker unmounts mid-animation.
Animated focusRect
MapKit applies focusRect instantly (the SDK has no animated setter), which
looks like a jump when a bottom sheet opens. focusRect now interpolates the
rect natively and can fit content into the final rect in the same motion:
await map.current.focusRect(
rect, // ScreenRect, in physical pixels as before
0.4, // seconds; omit or 0 → instant (previous behavior)
{ fitPoints: routePoints }, // camera animates in parallel so these points fit the final rect
);
// fitVisible keeps what the user currently sees instead of framing given
// points: the zoom shifts by log2 of the rect size ratio — symmetric and
// exactly reversible when the sheet goes back down.
map.current.focusRect(rect, 0, { fitVisible: true });The promise resolves when the transition finishes. A new focusRect call
cancels the previous animation.
For a map glued to a bottom sheet in real time use the declarative props on
AnimatedMapView instead — values flow from Reanimated's UI thread with no
JS-thread hop per frame:
const sheetPosition = useSharedValue(0); // <BottomSheet animatedPosition={sheetPosition} />
const scale = PixelRatio.get();
const focusRectProps = useAnimatedProps(() => ({
focusRect: {
topLeft: { x: 0, y: 0 },
bottomRight: { x: width * scale, y: (sheetPosition.value > 0 ? sheetPosition.value : height) * scale },
},
}));
<AnimatedMapView animatedProps={focusRectProps} focusRectFitVisible ... />Live marker content: tracksViewChanges
The placemark renders a snapshot of its children, so animated child views
(pulse effects) and async images freeze at the moment of capture — that is why
an Image inside a marker used to need a key remount hack. While
tracksViewChanges is set, the snapshot is retaken every UI frame:
<MarkerView point={point} tracksViewChanges={pulsing || !imageLoaded}>
<Image source={{ uri }} onLoad={() => setImageLoaded(true)} />
</MarkerView>Keep it enabled only while the content actually changes — each frame costs a
main-thread view render (same trade-off as tracksViewChanges in
react-native-maps). Default false.
Installation in managed Expo projects
For managed Expo projects, please follow the installation instructions in the API documentation for the latest stable release. If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
Installation in bare React Native projects
For bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.
Add the package to your npm dependencies
npm install expo-yandex-mapsConfigure for Android
Configure for iOS
Run npx pod-install after installing the npm package.
Contributing
Contributions are very welcome! Please refer to guidelines described in the contributing guide.
