Maps

Draw a real native map with react-native-maps: regions, markers, the API keys each platform needs, and many-marker performance.

Syntax<MapView style={{ flex: 1 }} initialRegion={{ latitude, longitude, latitudeDelta, longitudeDelta }} />

React Native ships no map component. react-native-maps adds one: npx expo install react-native-maps. What it renders is a genuine native map view, Apple Maps or Google Maps, not an image and not a web page.

A map shows a region

A region is four numbers: latitude, longitude, latitudeDelta and longitudeDelta. The deltas are the visible span in degrees, north to south and east to west, so smaller means more zoomed in. Pass initialRegion to set the opening view and then leave the user alone. Pass region only when you truly intend to drive the viewport from state, because a controlled region fights the user's finger if you update it carelessly. onRegionChangeComplete fires with the new region and a second argument containing isGesture, which distinguishes a user pan from your own animation. That flag is supplied only by the Google Maps renderer, so on Apple Maps it arrives as undefined; set provider={PROVIDER_GOOGLE}, or treat a missing flag as a gesture, if you mean to branch on it.

Markers

Markers are children of the map: <Marker coordinate={{ latitude, longitude }} title="Home" />. Boolean props follow the JSX rule, so showsUserLocation is shorthand for showsUserLocation={true}.

Keys per platform

iOS defaults to Apple Maps and needs no key. Android always uses Google Maps and needs one, supplied to the config plugin as androidGoogleMapsApiKey. Because a config plugin edits the native project, a changed key takes effect in the next build, not on a JavaScript reload.

Many markers

Every marker is a native view. Hundreds of them stutter. Filter to the visible region, cluster, and set tracksViewChanges={false} on markers with custom children once they have drawn.

Example

Example · tsx
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
import { StyleSheet, View } from "react-native";

type Stop = { id: string; title: string; latitude: number; longitude: number };

const stops: Stop[] = [
  { id: "a", title: "Warehouse", latitude: 51.5079, longitude: -0.0877 },
  { id: "b", title: "Cafe", latitude: 51.5123, longitude: -0.0912 },
  { id: "c", title: "Depot", latitude: 51.5041, longitude: -0.0801 },
];

export default function StopsMap() {
  return (
    <View style={styles.screen}>
      <MapView
        style={styles.map}
        // Google on both platforms. Omit this prop to get Apple Maps on iOS.
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: 51.5079,
          longitude: -0.0877,
          latitudeDelta: 0.02, // visible span north to south, in degrees
          longitudeDelta: 0.02, // visible span east to west
        }}
        showsUserLocation // shorthand for showsUserLocation={true}
      >
        {stops.map((stop) => (
          <Marker
            key={stop.id}
            coordinate={{ latitude: stop.latitude, longitude: stop.longitude }}
            title={stop.title}
          />
        ))}
      </MapView>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1 },
  map: { flex: 1 }, // without a height the map draws nothing
});

When to use it

  • Plot search results as pins and centre the map on the best match.
  • Let the user drag the map to choose a pickup point.
  • Show a driver the remaining stops on a delivery route.
  • Display one venue with a callout and a button to open directions.

More examples

Recentring without fighting the user

This is the pattern to copy instead of a controlled region prop. The map owns its own viewport; you move it imperatively through the ref with animateToRegion(region, duration). The isGesture flag keeps the Back to start button from appearing in response to your own animation, which is what makes the button flicker when people skip that check. One caveat: isGesture is only filled in by the Google Maps renderer, so this map needs provider={PROVIDER_GOOGLE} for the check to work on iOS. Left on Apple Maps, details.isGesture is undefined, the condition is never true, and the Back to start button never appears at all.

Example · tsx
import { useRef, useState } from "react";
import { Button, StyleSheet, View } from "react-native";
import MapView, { Marker } from "react-native-maps";

const home = {
  latitude: 48.8584,
  longitude: 2.2945,
  latitudeDelta: 0.01,
  longitudeDelta: 0.01,
};

export default function RecentreMap() {
  const mapRef = useRef<MapView | null>(null);
  const [drifted, setDrifted] = useState(false);

  return (
    <View style={styles.screen}>
      <MapView
        ref={mapRef}
        style={styles.map}
        initialRegion={home}
        onRegionChangeComplete={(_region, details) => {
          // isGesture is false when our own animateToRegion moved the map.
          if (details.isGesture) setDrifted(true);
        }}
      >
        <Marker coordinate={home} title="Eiffel Tower" />
      </MapView>
      {drifted ? (
        <View style={styles.bar}>
          <Button
            title="Back to start"
            onPress={() => {
              mapRef.current?.animateToRegion(home, 400); // 400 ms animation
              setDrifted(false);
            }}
          />
        </View>
      ) : null}
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1 },
  map: { flex: 1 },
  bar: { position: "absolute", bottom: 24, alignSelf: "center" },
});

The Android key in app.json

Android needs a Google Maps key or the map renders as a blank grey grid with a watermark. The iOS key is needed only if you set provider to PROVIDER_GOOGLE on iOS; with Apple Maps you need no key at all. Keys shipped in an app binary are readable, so restrict each one in the Google Cloud console to your Android package name with its SHA-1 fingerprint, and to your iOS bundle identifier. The plugin writes these into the native project during prebuild, so run a new build after changing them.

Example · json
{
  "expo": {
    "name": "Routes",
    "slug": "routes",
    "plugins": [
      [
        "react-native-maps",
        {
          "androidGoogleMapsApiKey": "AIza-your-android-key",
          "iosGoogleMapsApiKey": "AIza-your-ios-key"
        }
      ]
    ]
  }
}

Only render the markers you can see

A thousand Marker children means a thousand native views, and the map will drop frames while panning. Recompute the visible subset in onRegionChangeComplete, cap it, and render only that. Because the filter is a plain function it can be unit tested without a simulator, which the map itself cannot be.

Example · tsx
export type Pin = { id: string; latitude: number; longitude: number };
export type Box = {
  latitude: number;
  longitude: number;
  latitudeDelta: number;
  longitudeDelta: number;
};

// Keep this out of the component so it is easy to test.
export function pinsInRegion(pins: Pin[], region: Box, limit = 100): Pin[] {
  const halfLat = region.latitudeDelta / 2;
  const halfLon = region.longitudeDelta / 2;
  return pins
    .filter(
      (pin) =>
        Math.abs(pin.latitude - region.latitude) <= halfLat &&
        Math.abs(pin.longitude - region.longitude) <= halfLon,
    )
    .slice(0, limit); // a hard ceiling protects the frame rate
}

const pins: Pin[] = [
  { id: "a", latitude: 51.5, longitude: -0.09 },
  { id: "b", latitude: 51.9, longitude: -0.09 },
];
const region: Box = {
  latitude: 51.5,
  longitude: -0.09,
  latitudeDelta: 0.2,
  longitudeDelta: 0.2,
};

console.log(pinsInRegion(pins, region).map((pin) => pin.id));
// Output: [ 'a' ]

Discussion

  • Be the first to comment on this lesson.