Moving Beyond the Basics: Why Real-Time Location Matters

User location is one of the most powerful data streams a mobile application can capture. When done right, it turns a static app into a context-aware tool that anticipates what users need. React Native Maps combined with the Geolocation API is the industry-standard approach for this, and they integrate naturally with a headless CMS like Directus. By storing and serving location data through Directus, you can build features ranging from driver tracking and delivery routing to social check-ins and proximity-based recommendations. This article walks through a full implementation, from environment setup to production-hardened best practices, so you can ship a reliable location-aware experience.

Architecture Overview: Where Directus Fits In

Before writing any code, it helps to understand how the pieces connect. React Native handles the mobile frontend, including map rendering and raw location data. Directus acts as the backend and headless CMS. It stores user location histories, geofence definitions, and any metadata you need. The React Native app fetches and sends location data to Directus via its REST or GraphQL API. This separation keeps your frontend lean and your data centralized, searchable, and auditable.

For a deeper look at Directus's data modeling capabilities, see the Directus Data Modeling Guide.

Prerequisites and Package Setup

You'll need a working React Native project (CLI or Expo bare workflow) and a Directus instance. For local development, you can spin up Directus via Docker. Here are the core packages you need in your React Native project:

  • react-native-maps – Provides the map component and marker overlay.
  • @react-native-community/geolocation – Access to device GPS and network location.
  • @react-native-community/geolocation – Access to device GPS and network location.

Install them from your project root:

npm install react-native-maps @react-native-community/geolocation

For iOS, run cd ios && pod install to link the native modules. If you're using Expo, you will need to eject to the bare workflow or use the expo-dev-client because these libraries require native module linking. After installation, verify everything compiles by running the app on a simulator or device before moving on.

Platform Permissions: The Gatekeeper

Both Android and iOS require explicit user consent to access location. Failing to handle permissions gracefully is the number one cause of runtime crashes and negative reviews. Start with the required platform configuration.

Android Configuration

Open android/app/src/main/AndroidManifest.xml and add these permissions inside the <manifest> block, before the <application> element:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

ACCESS_FINE_LOCATION gives GPS accuracy, while ACCESS_COARSE_LOCATION provides a fallback using Wi-Fi and cell towers. Including both lets your app degrade gracefully in environments where GPS is weak, such as indoors.

iOS Configuration

Open ios/YourProject/Info.plist and add one or both of these keys:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to show nearby services and track your route.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs background location access to provide continuous route tracking.</string>

Apple requires a compelling explanation for each key. Avoid generic strings like “Your app needs access to your location,” as reviewers may reject it. Be specific: “To show nearby coffee shops and provide turn-by-turn navigation,” for example. Start with NSLocationWhenInUseUsageDescription if you only need foreground tracking. Upgrade to the always key only if your app genuinely requires background updates.

Runtime Permission Handling in React Native

Setting manifest values is only half the work. You also need to request permission at runtime, especially on Android 6.0+ and iOS 8.0+. Use the PermissionsAndroid module for Android and rely on the native geolocation library for iOS, which triggers the system prompt automatically when you call getCurrentPosition or watchPosition.

Here is a reusable permission hook:

import { PermissionsAndroid, Platform } from 'react-native';

const requestLocationPermission = async () => {
  if (Platform.OS === 'android') {
    const apiLevel = Platform.Version;
    if (apiLevel < 23) {
      // permissions granted automatically at install time
      return true;
    }
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
      {
        title: 'Location Permission',
        message: 'This app needs access to your location.',
        buttonPositive: 'Grant',
        buttonNegative: 'Deny',
      }
    );
    return granted === PermissionsAndroid.RESULTS.GRANTED;
  }
  // iOS handles runtime permission via the Geolocation call itself
  return true;
};

Call this function before any geolocation call. If the user denies, display a friendly message explaining the impact and offer a button to open system settings. Never simply crash or show a blank map.

Core Implementation: Displaying the User on a Map

With permissions wired up, you can build the main tracking component. The following code requests the user's current position, sets the map region, and drops a marker. It also updates the marker in real time using watchPosition.

import React, { useState, useEffect, useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation';
import { requestLocationPermission } from './permission';

const LocationTracker = () => {
  const [region, setRegion] = useState(null);
  const [currentLocation, setCurrentLocation] = useState(null);
  const watchId = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    const initTracking = async () => {
      const hasPermission = await requestLocationPermission();
      if (!hasPermission) return;

      // Get the initial position immediately
      Geolocation.getCurrentPosition(
        (position) => {
          const { latitude, longitude } = position.coords;
          const initialRegion = {
            latitude,
            longitude,
            latitudeDelta: 0.01,
            longitudeDelta: 0.01,
          };
          setRegion(initialRegion);
          setCurrentLocation({ latitude, longitude });
        },
        (error) => {
          console.warn('Initial position error:', error.message);
        },
        { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
      );

      // Subscribe to continuous updates
      watchId.current = Geolocation.watchPosition(
        (position) => {
          const { latitude, longitude } = position.coords;
          setCurrentLocation({ latitude, longitude });
          // Optionally animate the map to follow the user
          mapRef.current?.animateToRegion({
            latitude,
            longitude,
            latitudeDelta: 0.01,
            longitudeDelta: 0.01,
          }, 500);
        },
        (error) => {
          console.warn('Watch position error:', error.message);
        },
        { enableHighAccuracy: true, distanceFilter: 10, interval: 5000 }
      );
    };

    initTracking();

    return () => {
      if (watchId.current !== null) {
        Geolocation.clearWatch(watchId.current);
      }
    };
  }, []);

  if (!region) {
    return ;
  }

  return (
    
      
        {currentLocation && (
          
        )}
      
    
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { flex: 1 },
});

export default LocationTracker;

Key details in this implementation:

  • distanceFilter: 10 – Only triggers a location update when the device moves at least 10 meters. This saves battery and reduces network calls.
  • interval: 5000 – Caps the update frequency to once every 5 seconds on Android. iOS ignores this but uses its own throttling.
  • showsUserLocation – A built-in React Native Maps feature that displays a native system dot for the user's location, even without an explicit marker. It also handles the blue pulsing accuracy circle.

Persisting Location Data to Directus

Displaying the location on a map is useful, but the real value comes from storing and querying that data. In Directus, create a collection called locations with these fields:

  • id (auto-increment, primary key)
  • user_id (string or relation to directus_users)
  • latitude (float)
  • longitude (float)
  • accuracy (float, meters)
  • timestamp (datetime, auto-generated)

Then, inside your watchPosition callback, send each update to Directus:

import axios from 'axios';

const DIRECTUS_URL = 'https://your-directus-instance.com';
const DIRECTUS_TOKEN = 'your-static-token-or-dynamic-jwt';

const saveLocationToDirectus = async (coords) => {
  try {
    await axios.post(
      `${DIRECTUS_URL}/items/locations`,
      {
        latitude: coords.latitude,
        longitude: coords.longitude,
        accuracy: coords.accuracy,
        // user_id is typically pulled from auth context
      },
      {
        headers: {
          Authorization: `Bearer ${DIRECTUS_TOKEN}`,
        },
      }
    );
  } catch (error) {
    console.error('Failed to save location:', error.response?.data || error.message);
  }
};

Batch updates if you expect high-frequency writes. Directus supports creating multiple items at once by passing an array to the same endpoint. This reduces network overhead and avoids rate limits.

Geofencing with React Native and Directus

Geofencing is the ability to detect when a user enters or exits a predefined geographic boundary. Directus can store these boundary definitions, and your React Native app can check them against the current position. This pattern powers notifications like “Welcome to the store!” or “You left your office area.”

Storing Geofences in Directus

Create a collection called geofences with the following fields:

  • id (primary key)
  • name (string)
  • latitude (float)
  • longitude (float)
  • radius (float, meters)
  • trigger_on_entry (boolean)
  • trigger_on_exit (boolean)
  • message (text)

Client-Side Geofence Check

On each location update, fetch the active geofences from Directus and test the distance between the current position and each fence center. You can use the Haversine formula or a simpler approximation:

const getDistanceFromLatLonInMeters = (lat1, lon1, lat2, lon2) => {
  const R = 6371000; // Earth's radius in meters
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) *
      Math.cos(lat2 * Math.PI / 180) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
};

const checkGeofences = async (currentCoords, geofences) => {
  for (const fence of geofences) {
    const distance = getDistanceFromLatLonInMeters(
      currentCoords.latitude,
      currentCoords.longitude,
      fence.latitude,
      fence.longitude
    );
    if (distance <= fence.radius) {
      return fence; // user is inside this geofence
    }
  }
  return null;
};

Cache the geofences list locally and refresh it periodically rather than fetching on every position update. This keeps the app responsive and reduces server load.

Performance and Battery Optimization

Location tracking is one of the most battery-intensive features on mobile. Users will uninstall an app that drains their phone, so optimization is not optional. Follow these guidelines:

  • Use distanceFilter – Set it high enough to avoid jittery updates. For walking apps, 10 meters is a good baseline. For driving apps, 20-50 meters works better.
  • Reduce location accuracy when possible – For features like “current city,” coarse location (network-based) is sufficient. Use fine (GPS) only when accuracy below 50 meters matters.
  • Throttle network calls – Buffer location points in memory and flush them to Directus every 30 seconds or every 100 points, whichever comes first.
  • Stop watching when the app goes to background – Unless your app has a legitimate background use case (navigation, fitness tracking), clear the watchId in the AppState change listener. iOS will terminate your app anyway if it uses too much background energy.
  • Use maximumAge – Accept a cached location if it's recent enough. This avoids waking the GPS hardware unnecessarily.

Apple and Google both provide detailed energy profiling tools. Use Xcode's Energy Log and Android's Battery Historian to inspect your app's behavior before shipping.

Error Handling and Edge Cases

Location services are notoriously unreliable. Users may deny permissions, airplane mode may be on, GPS may be blocked indoors, and older devices can return stale or wildly inaccurate data. Your app should anticipate every one of these scenarios.

  • Permission denied – Show a non-blocking banner with a “Open Settings” button. Never force-quit the app.
  • Location unavailable – Display a message like “Location temporarily unavailable. Check your device settings.” Continue watching so the app recovers automatically when the signal returns.
  • Timeout – Retry up to three times with exponential backoff. If it still fails, fall back to coarse location or the last known position.
  • Stale or cached data – Compare the timestamp of the returned position against Date.now(). If it's older than 60 seconds, discard it and wait for the next update.

Integrate a lightweight logging service (or simply send errors to a Directus collection called location_errors) so you can monitor real-world failure rates and improve your logic iteratively.

Testing Across Devices and Scenarios

Simulators are useful for quick UI iteration, but they cannot simulate real GPS behavior. Always test on physical devices under varying conditions: walking, driving, inside a building, in a tunnel, and with the device in power-saving mode. Android emulators can inject mock locations via the extended controls, but iOS simulators cannot emulate GPS at all for the geolocation library. Use a service like Locationary to replay realistic GPS tracks during development.

Create a test plan that covers these cases:

  • Fresh install and first-time permission prompt
  • Denying permission and then granting it later via settings
  • Rapid movement (driving at highway speeds)
  • Stationary device with high accuracy (should produce a tight cluster of points)
  • Background-to-foreground transition (did the map recover the last known position?)

Privacy, Compliance, and User Trust

Location data is sensitive. Mishandling it can lead to regulatory fines and loss of user trust. Treat it with the same care as passwords or financial information.

  • Transparency – Explain exactly what location data you collect and why in your privacy policy. Link to it from the permission prompt.
  • Data minimization – Collect only the accuracy level you need. Delete old points after a retention period (for example, 30 days) via a Directus automation flow.
  • Encryption – Serve your Directus instance over HTTPS. Consider encrypting the locations collection fields at rest if your threat model demands it.
  • User controls – Provide an in-app dashboard where users can see their location history and delete individual points or wipe the entire history. This builds trust and aligns with privacy regulations like GDPR and CCPA.

For more guidance on handling personal data in Directus, review the Directus Security Configuration Options.

Going Further: Advanced Use Cases

Once the basics are solid, you can layer on more sophisticated features using the same foundation.

Realtime Location Sharing

Use Directus's WebSocket or SSE support to broadcast location updates to other users. For example, a delivery app can show the courier's moving marker on the customer's map. The backend receives the location via REST and pushes it out to subscribed clients without polling.

Location-Based Analytics

Aggregate location data in Directus to generate heatmaps, dwell time reports, or popular route patterns. The Directus Analytics Guide shows how to build dashboards on top of your data.

Offline Queueing

When the network is unavailable, queue location points on the device using AsyncStorage or a local database like WatermelonDB. Once connectivity resumes, flush the queue to Directus. This ensures data integrity even in areas with spotty coverage.

Conclusion

Implementing user location tracking with React Native Maps and the Geolocation API is a well-understood pattern, but the devil is in the details. By pairing a robust frontend implementation with a headless CMS like Directus, you gain a flexible backend for storing, querying, and acting on location data without building a custom API layer. Focus on permission handling, battery optimization, and data privacy from the start, and you will deliver a feature that users find genuinely useful rather than intrusive. The code examples and strategies in this guide give you a production-ready foundation that you can adapt to domains like logistics, social networking, field services, or any app that benefits from knowing where its users are.