+1 (415) 943-4271

Maps and Location in React Native: Picking a Map SDK, Background Location, and Geofencing That Survives Review

Almost every field-service, delivery, fitness, or marketplace app we are brought in to rescue has the same two problems: the map janks once real data lands on it, and the location permissions were bolted on late, so the app either drains the battery or gets rejected. Both are avoidable if you make the decisions in the right order.

Step 1: pick the map SDK for your constraints, not your preference

OptionGood fit whenWatch out for
react-native-maps (Apple Maps / Google Maps)Standard pins, routes, and "where is my driver" screens; you want the platform-native lookGoogle Maps on Android needs an API key and billing; styling options are limited
Mapbox (@rnmapbox/maps)Custom cartography, vector styling, offline regions, heavy data layersUsage-based pricing that scales with monthly active users; larger binary
MapLibreYou want vector maps with no vendor lock-in and can host or buy your own tilesYou own the tile bill and the styling work

A rule of thumb from engagements: if the map is a feature, react-native-maps is usually enough. If the map is the product, budget for Mapbox or MapLibre and for someone to own the style spec.

All three ship native code, so on Expo you install the config plugin and use a development build — none of them run in Expo Go:

{
  "expo": {
    "plugins": [
      ["react-native-maps", { "iosGoogleMapsApiKey": "..." }],
      ["expo-location", {
        "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to track jobs while you drive between sites."
      }]
    ]
  }
}

Step 2: keep the map off the JS thread

Maps jank for predictable reasons. In order of how often we find them:

  1. Markers re-created every render. Memoize marker components and keep the marker array in state that only changes when the data does. A useMemo over your query result is usually the entire fix.
  2. No clustering. Anything past a few hundred pins needs clustering — react-native-map-clustering for react-native-maps, or a ShapeSource with cluster enabled on Mapbox. Clustering also fixes the "user taps the wrong pin" complaint.
  3. Custom marker views with images. Each custom <Marker> on Android is a native view backed by a bitmap. Render a lightweight component, or pass a pre-rendered image, and never put an animated component inside a marker.
  4. Region updates fighting the gesture. Do not drive region from state on every onRegionChange. Use onRegionChangeComplete, or let the map own its camera and call animateToRegion imperatively.
  5. Live vehicle positions in React state. For frequently-moving markers, drive coordinates with Reanimated shared values or AnimatedRegion so updates stay off the JS render path.
const markers = useMemo(
  () => jobs.map((j) => (
    <Marker key={j.id} coordinate={j.coord} tracksViewChanges={false} />
  )),
  [jobs]
);

tracksViewChanges={false} after first paint is the single highest-leverage prop on iOS marker performance. Leave it true only while a marker's visual content is still loading.

Step 3: request the smallest permission that does the job

Location permission comes in tiers, and each tier up costs you install-to-activation conversion and review scrutiny:

  • When in use — the default. Covers "show me nearby", check-in flows, and one-off address capture.
  • Always / background — only if the app must record or react to location while backgrounded.
  • Precise vs approximate — iOS 14+ and Android 12+ let users grant coarse location. Handle it: if your feature works at neighbourhood resolution, do not nag.
import * as Location from "expo-location";

const fg = await Location.requestForegroundPermissionsAsync();
if (fg.status !== "granted") return showRationaleScreen();

// Only after the user has seen the feature work in the foreground:
const bg = await Location.requestBackgroundPermissionsAsync();

Ask for background second, in context, after the user has used the feature. Both platforms require a plain-language purpose string, and Apple reviewers routinely reject apps whose background usage string does not match an obvious user-facing benefit. Android 10+ additionally puts background location behind a separate settings screen, so your UI needs a "take me to settings" fallback rather than an infinite permission loop.

Step 4: geofencing instead of polling

The naive implementation — a background timer that reads GPS every minute — is a battery complaint and an OS-throttling problem. Use the platform geofencing APIs, which are handled by the OS radio and cost nearly nothing when the user is not moving:

import * as TaskManager from "expo-task-manager";
import * as Location from "expo-location";

TaskManager.defineTask("geofence", ({ data, error }) => {
  if (error) return;
  const { eventType, region } = data as any;
  if (eventType === Location.GeofencingEventType.Enter) {
    queueArrival(region.identifier);
  }
});

await Location.startGeofencingAsync("geofence", [
  { identifier: "site-42", latitude: 51.5, longitude: -0.12, radius: 150 }
]);

Four constraints to design around:

  • iOS allows 20 monitored regions per app. With more sites than that, monitor the nearest N and re-register as the user moves.
  • Radii under ~100 m fire unreliably. Treat geofences as a coarse trigger, then confirm with a single foreground fix.
  • Events arrive with the app killed. The task handler must be registered at module scope, not inside a component, and must do its work without touching your React tree.
  • Enter/exit can duplicate or arrive late. Make arrival handling idempotent and timestamped server-side.

For continuous tracking (a delivery route, a run), use startLocationUpdatesAsync with activityType, a sane distanceInterval, and a foreground service notification on Android — never a JS interval.

Step 5: budget for offline and for privacy disclosure

Field apps lose signal. Cache the last known position, queue location-tagged writes the way you would any offline-first mutation, and pre-download offline map regions if your users work in basements or rural sites. On the compliance side, location is a declared data type: it belongs in your iOS privacy manifest and Play Data Safety form, and if a third-party analytics or attribution SDK also receives coordinates, that has to be disclosed too.

A sensible default stack

For most teams starting today: react-native-maps with clustering, expo-location for permissions and geofencing, foreground-only at first launch, background requested in context, and a server that treats every location event as possibly duplicated and possibly hours late. Reach for Mapbox when the cartography itself is the differentiator.

If you are scoping a location-heavy React Native build — or debugging a map that crawls on mid-range Android — get in touch. Our consultants have shipped this pattern for dispatch, logistics, and field-service apps.