Implementing Background Tasks and Geofencing in React Native
Introduction
Location-based services have become essential in modern mobile applications, from delivery tracking and ride-sharing to fitness apps and smart home automation. Two powerful features that enable these experiences are background tasks and geofencing. Background tasks allow your app to execute code even when it's not in the foreground, while geofencing enables your app to respond to users entering or exiting specific geographical areas.
In this comprehensive guide, we'll explore how to implement both background tasks and geofencing in React Native applications. You'll learn how to handle location updates in the background, set up virtual perimeters, and trigger actions when users cross geofence boundaries - all while respecting battery life and user privacy.
What Are Background Tasks?
Background tasks are operations that continue to run even when your app is minimized, closed, or the device is locked. In the context of React Native, background tasks are essential for:
- Location tracking: Monitoring user location for fitness, navigation, or delivery apps
- Data synchronization: Syncing data with servers at regular intervals
- Push notifications: Processing incoming notifications and updating app state
- Sensor monitoring: Collecting data from device sensors over time
- Scheduled tasks: Running periodic maintenance or cleanup operations
What is Geofencing?
Geofencing is a location-based service that triggers an action when a device enters or exits a predefined geographical boundary (a "geofence"). A geofence is typically defined by:
- Center point: Latitude and longitude coordinates
- Radius: Distance from the center point (usually in meters)
- Trigger events: Entry, exit, or dwelling within the geofence
Common use cases include:
- Retail: Send promotional notifications when users enter a store's vicinity
- Smart home: Automatically adjust home settings based on proximity
- Time tracking: Clock in/out employees when they arrive at work locations
- Reminders: Trigger location-based reminders (e.g., "Buy milk when near grocery store")
- Asset tracking: Monitor vehicles or equipment entering/leaving facilities
Why These Features Matter
Combining background tasks with geofencing creates powerful user experiences:
- Contextual awareness: Apps can respond intelligently to user location without requiring constant user interaction
- Improved engagement: Timely, location-relevant notifications increase user engagement
- Automation: Enable hands-free experiences based on location
- Offline capability: Continue tracking and processing even without active app usage
- Battery efficiency: Modern geofencing APIs are optimized for minimal battery impact
Prerequisites
Before we begin, ensure you have:
- React Native 0.68 or higher
- Node.js 14 or higher
- A physical device for testing (geofencing works poorly on simulators)
- Basic understanding of React Native and async/await patterns
- Familiarity with React hooks and state management
Important: Background location tracking and geofencing have significant privacy implications. Always be transparent with users about why you need these permissions and how you'll use their data.
Project Setup
Let's start by creating a new React Native project or using an existing one:
npx react-native init GeofencingApp --template react-native-template-typescript
cd GeofencingApp
Installing Dependencies
We'll use several libraries to implement background tasks and geofencing:
npm install @react-native-community/geolocation react-native-background-fetch react-native-geolocation-service
# or
yarn add @react-native-community/geolocation react-native-background-fetch react-native-geolocation-service
For geofencing specifically, we'll also need:
npm install react-native-background-geolocation
# or
yarn add react-native-background-geolocation
Note: The react-native-background-geolocation
package offers a free tier for testing and a commercial license for production use. Alternatively, you can implement geofencing using platform-specific native modules.
For iOS, install CocoaPods dependencies:
cd ios && pod install && cd ..
iOS Configuration
Info.plist Setup
iOS requires explicit permission descriptions for location access. Open ios/GeofencingApp/Info.plist
and add:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need access to your location to provide location-based features and send you relevant notifications when you enter specific areas.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need access to your location to show your current position on the map.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>We need access to your location in the background to track your activity and send you location-based notifications.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
<string>processing</string>
</array>
Capabilities
In Xcode, open your project and navigate to:
- Select your target
- Go to "Signing & Capabilities"
- Click "+ Capability"
- Add "Background Modes"
- Check "Location updates" and "Background fetch"
Android Configuration
AndroidManifest.xml Setup
Open android/app/src/main/AndroidManifest.xml
and add the necessary permissions:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Android 10+ background location (must request separately) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- For background tasks -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- For notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- ... other configuration ... -->
</application>
</manifest>
Gradle Configuration
Ensure your android/build.gradle
has the correct versions:
buildscript {
ext {
buildToolsVersion = "33.0.0"
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
}
}
Permission Request Helper
Let's create a utility to request location permissions properly. Create utils/PermissionHelper.ts
:
import { Platform, PermissionsAndroid, Alert, Linking } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
export type PermissionLevel = 'whenInUse' | 'always';
class PermissionHelper {
/**
* Request location permissions based on the specified level
*/
async requestLocationPermission(
level: PermissionLevel = 'whenInUse'
): Promise<boolean> {
if (Platform.OS === 'ios') {
return this.requestIOSPermission(level);
} else {
return this.requestAndroidPermission(level);
}
}
/**
* iOS permission request
*/
private async requestIOSPermission(
level: PermissionLevel
): Promise<boolean> {
try {
const status = await Geolocation.requestAuthorization(
level === 'always' ? 'always' : 'whenInUse'
);
if (status === 'granted') {
return true;
}
if (status === 'denied') {
this.showPermissionDeniedAlert();
return false;
}
return false;
} catch (error) {
console.error('Error requesting iOS location permission:', error);
return false;
}
}
/**
* Android permission request
*/
private async requestAndroidPermission(
level: PermissionLevel
): Promise<boolean> {
try {
// First, request foreground location permission
const foregroundGranted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location Permission',
message: 'This app needs access to your location to provide location-based features.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
if (foregroundGranted !== PermissionsAndroid.RESULTS.GRANTED) {
return false;
}
// If always permission is requested and Android 10+, request background location
if (level === 'always' && Platform.Version >= 29) {
const backgroundGranted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION,
{
title: 'Background Location Permission',
message:
'To enable geofencing and background tracking, please allow location access all the time.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
return backgroundGranted === PermissionsAndroid.RESULTS.GRANTED;
}
return true;
} catch (error) {
console.error('Error requesting Android location permission:', error);
return false;
}
}
/**
* Check if location permission is granted
*/
async hasLocationPermission(): Promise<boolean> {
if (Platform.OS === 'ios') {
const status = await Geolocation.requestAuthorization('whenInUse');
return status === 'granted';
} else {
const granted = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
return granted;
}
}
/**
* Check if background location permission is granted
*/
async hasBackgroundPermission(): Promise<boolean> {
if (Platform.OS === 'ios') {
// iOS doesn't provide a way to check this programmatically
// You need to check the authorization status
return true; // Simplified for this example
} else {
if (Platform.Version >= 29) {
return await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION
);
}
return true; // Android < 10 doesn't have separate background permission
}
}
/**
* Show alert when permission is denied
*/
private showPermissionDeniedAlert() {
Alert.alert(
'Location Permission Required',
'This app needs location access to work properly. Please enable it in your device settings.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
]
);
}
}
export default new PermissionHelper();
Implementing Background Location Tracking
Now let's create a service to handle background location tracking. Create services/LocationService.ts
:
import Geolocation from 'react-native-geolocation-service';
import { Platform } from 'react-native';
export interface LocationData {
latitude: number;
longitude: number;
accuracy: number;
altitude: number | null;
heading: number | null;
speed: number | null;
timestamp: number;
}
export interface LocationConfig {
distanceFilter: number; // Minimum distance (in meters) to trigger update
desiredAccuracy: {
ios: number;
android: number;
};
interval: number; // Android only (milliseconds)
fastestInterval: number; // Android only (milliseconds)
showsBackgroundLocationIndicator?: boolean; // iOS only
}
class LocationService {
private watchId: number | null = null;
private locationCallback: ((location: LocationData) => void) | null = null;
private errorCallback: ((error: any) => void) | null = null;
/**
* Start tracking location in the background
*/
startTracking(
onLocation: (location: LocationData) => void,
onError: (error: any) => void,
config?: Partial<LocationConfig>
): void {
const defaultConfig: LocationConfig = {
distanceFilter: 10, // Update every 10 meters
desiredAccuracy: {
ios: Geolocation.ACCURACY_BEST_FOR_NAVIGATION,
android: Geolocation.ACCURACY_HIGH,
},
interval: 5000, // 5 seconds
fastestInterval: 2000, // 2 seconds
showsBackgroundLocationIndicator: true,
};
const finalConfig = { ...defaultConfig, ...config };
this.locationCallback = onLocation;
this.errorCallback = onError;
const options = {
accuracy: {
android: finalConfig.desiredAccuracy.android,
ios: finalConfig.desiredAccuracy.ios,
},
distanceFilter: finalConfig.distanceFilter,
interval: finalConfig.interval,
fastestInterval: finalConfig.fastestInterval,
enableHighAccuracy: true,
showLocationDialog: true,
forceRequestLocation: true,
showsBackgroundLocationIndicator:
finalConfig.showsBackgroundLocationIndicator,
useSignificantChanges: false, // Set to true for battery savings
};
this.watchId = Geolocation.watchPosition(
(position) => {
const locationData: LocationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp,
};
if (this.locationCallback) {
this.locationCallback(locationData);
}
},
(error) => {
console.error('Location tracking error:', error);
if (this.errorCallback) {
this.errorCallback(error);
}
},
options
);
}
/**
* Stop tracking location
*/
stopTracking(): void {
if (this.watchId !== null) {
Geolocation.clearWatch(this.watchId);
this.watchId = null;
this.locationCallback = null;
this.errorCallback = null;
}
}
/**
* Get current location (one-time)
*/
async getCurrentLocation(): Promise<LocationData> {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp,
});
},
(error) => {
reject(error);
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 10000,
}
);
});
}
/**
* Calculate distance between two coordinates (in meters)
* Uses the Haversine formula
*/
calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371e3; // Earth's radius in meters
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
}
export default new LocationService();
Implementing Geofencing
Now let's create a geofencing service. Create services/GeofenceService.ts
:
import { Platform, NativeEventEmitter, NativeModules } from 'react-native';
import LocationService, { LocationData } from './LocationService';
export interface Geofence {
id: string;
latitude: number;
longitude: number;
radius: number; // in meters
title?: string;
description?: string;
}
export interface GeofenceEvent {
geofence: Geofence;
type: 'enter' | 'exit' | 'dwell';
location: LocationData;
timestamp: number;
}
type GeofenceCallback = (event: GeofenceEvent) => void;
class GeofenceService {
private geofences: Map<string, Geofence> = new Map();
private activeGeofences: Set<string> = new Set(); // Currently inside these geofences
private callbacks: Set<GeofenceCallback> = new Set();
private isMonitoring: boolean = false;
private lastLocation: LocationData | null = null;
/**
* Add a geofence to monitor
*/
addGeofence(geofence: Geofence): void {
this.geofences.set(geofence.id, geofence);
console.log(`Geofence added: ${geofence.id}`);
}
/**
* Remove a geofence
*/
removeGeofence(geofenceId: string): void {
this.geofences.delete(geofenceId);
this.activeGeofences.delete(geofenceId);
console.log(`Geofence removed: ${geofenceId}`);
}
/**
* Remove all geofences
*/
clearGeofences(): void {
this.geofences.clear();
this.activeGeofences.clear();
console.log('All geofences cleared');
}
/**
* Start monitoring geofences
*/
startMonitoring(callback: GeofenceCallback): void {
if (this.isMonitoring) {
console.warn('Geofence monitoring is already active');
return;
}
this.callbacks.add(callback);
this.isMonitoring = true;
// Start location tracking
LocationService.startTracking(
(location) => this.handleLocationUpdate(location),
(error) => console.error('Geofence location error:', error),
{
distanceFilter: 10, // Check every 10 meters
interval: 5000,
fastestInterval: 2000,
}
);
console.log('Geofence monitoring started');
}
/**
* Stop monitoring geofences
*/
stopMonitoring(): void {
if (!this.isMonitoring) {
return;
}
LocationService.stopTracking();
this.isMonitoring = false;
this.callbacks.clear();
this.activeGeofences.clear();
console.log('Geofence monitoring stopped');
}
/**
* Handle location updates and check geofences
*/
private handleLocationUpdate(location: LocationData): void {
this.lastLocation = location;
// Check each geofence
this.geofences.forEach((geofence) => {
const distance = LocationService.calculateDistance(
location.latitude,
location.longitude,
geofence.latitude,
geofence.longitude
);
const isInside = distance <= geofence.radius;
const wasInside = this.activeGeofences.has(geofence.id);
// Enter event
if (isInside && !wasInside) {
this.activeGeofences.add(geofence.id);
this.triggerEvent({
geofence,
type: 'enter',
location,
timestamp: Date.now(),
});
}
// Exit event
else if (!isInside && wasInside) {
this.activeGeofences.delete(geofence.id);
this.triggerEvent({
geofence,
type: 'exit',
location,
timestamp: Date.now(),
});
}
// Dwell event (optional - can be enhanced with time tracking)
else if (isInside && wasInside) {
// You can add logic here to trigger dwell events after a certain time
}
});
}
/**
* Trigger geofence event to all callbacks
*/
private triggerEvent(event: GeofenceEvent): void {
console.log(
`Geofence ${event.type}: ${event.geofence.id} (${event.geofence.title})`
);
this.callbacks.forEach((callback) => {
try {
callback(event);
} catch (error) {
console.error('Error in geofence callback:', error);
}
});
}
/**
* Get all geofences
*/
getGeofences(): Geofence[] {
return Array.from(this.geofences.values());
}
/**
* Get active geofences (user is currently inside)
*/
getActiveGeofences(): Geofence[] {
return Array.from(this.activeGeofences)
.map((id) => this.geofences.get(id))
.filter((g): g is Geofence => g !== undefined);
}
/**
* Check if user is currently inside a specific geofence
*/
isInsideGeofence(geofenceId: string): boolean {
return this.activeGeofences.has(geofenceId);
}
/**
* Get distance to a specific geofence (from last known location)
*/
getDistanceToGeofence(geofenceId: string): number | null {
const geofence = this.geofences.get(geofenceId);
if (!geofence || !this.lastLocation) {
return null;
}
return LocationService.calculateDistance(
this.lastLocation.latitude,
this.lastLocation.longitude,
geofence.latitude,
geofence.longitude
);
}
}
export default new GeofenceService();
Background Task Implementation
Let's set up background tasks using react-native-background-fetch
. Create services/BackgroundTaskService.ts
:
import BackgroundFetch from 'react-native-background-fetch';
import { Platform } from 'react-native';
export interface BackgroundTaskConfig {
minimumFetchInterval: number; // iOS minimum fetch interval (in minutes)
stopOnTerminate: boolean; // Android - stop background tasks when app is terminated
startOnBoot: boolean; // Android - start background tasks on device boot
enableHeadless: boolean; // Android - allow tasks to run even when app is terminated
requiresNetworkConnectivity?: boolean;
requiresCharging?: boolean;
requiresBatteryNotLow?: boolean;
requiresStorageNotLow?: boolean;
}
type TaskCallback = () => Promise<void>;
class BackgroundTaskService {
private isConfigured: boolean = false;
private taskCallbacks: Map<string, TaskCallback> = new Map();
/**
* Initialize and configure background fetch
*/
async configure(config?: Partial<BackgroundTaskConfig>): Promise<void> {
if (this.isConfigured) {
console.warn('Background tasks already configured');
return;
}
const defaultConfig: BackgroundTaskConfig = {
minimumFetchInterval: 15, // iOS minimum is 15 minutes
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresNetworkConnectivity: false,
requiresCharging: false,
requiresBatteryNotLow: false,
requiresStorageNotLow: false,
};
const finalConfig = { ...defaultConfig, ...config };
try {
const status = await BackgroundFetch.configure(
finalConfig,
async (taskId) => {
console.log('[BackgroundFetch] Task executed:', taskId);
await this.executeTask(taskId);
BackgroundFetch.finish(taskId);
},
(taskId) => {
console.log('[BackgroundFetch] Task timeout:', taskId);
BackgroundFetch.finish(taskId);
}
);
console.log('[BackgroundFetch] Status:', status);
this.isConfigured = true;
} catch (error) {
console.error('[BackgroundFetch] Configuration error:', error);
throw error;
}
}
/**
* Register a background task
*/
registerTask(taskId: string, callback: TaskCallback): void {
this.taskCallbacks.set(taskId, callback);
console.log(`[BackgroundFetch] Task registered: ${taskId}`);
}
/**
* Unregister a background task
*/
unregisterTask(taskId: string): void {
this.taskCallbacks.delete(taskId);
console.log(`[BackgroundFetch] Task unregistered: ${taskId}`);
}
/**
* Schedule a one-time task (Android only)
*/
async scheduleTask(
taskId: string,
delayMinutes: number = 15
): Promise<void> {
if (Platform.OS !== 'android') {
console.warn('scheduleTask is only available on Android');
return;
}
try {
await BackgroundFetch.scheduleTask({
taskId,
delay: delayMinutes * 60 * 1000, // Convert to milliseconds
periodic: false,
forceAlarmManager: false,
stopOnTerminate: false,
startOnBoot: true,
});
console.log(`[BackgroundFetch] Task scheduled: ${taskId}`);
} catch (error) {
console.error('[BackgroundFetch] Schedule task error:', error);
throw error;
}
}
/**
* Execute a registered task
*/
private async executeTask(taskId: string): Promise<void> {
const callback = this.taskCallbacks.get(taskId);
if (callback) {
try {
await callback();
console.log(`[BackgroundFetch] Task completed: ${taskId}`);
} catch (error) {
console.error(`[BackgroundFetch] Task error (${taskId}):`, error);
}
} else {
console.warn(`[BackgroundFetch] No callback found for task: ${taskId}`);
}
}
/**
* Stop all background tasks
*/
async stop(): Promise<void> {
try {
await BackgroundFetch.stop();
this.isConfigured = false;
console.log('[BackgroundFetch] Stopped');
} catch (error) {
console.error('[BackgroundFetch] Stop error:', error);
}
}
/**
* Get background fetch status
*/
async getStatus(): Promise<number> {
return await BackgroundFetch.status();
}
}
export default new BackgroundTaskService();
Creating a Demo App
Now let's create a complete demo app that showcases all these features. Create screens/GeofenceDemo.tsx
:
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Alert,
Switch,
TextInput,
FlatList,
} from 'react-native';
import PermissionHelper from '../utils/PermissionHelper';
import LocationService, { LocationData } from '../services/LocationService';
import GeofenceService, {
Geofence,
GeofenceEvent,
} from '../services/GeofenceService';
import BackgroundTaskService from '../services/BackgroundTaskService';
const GeofenceDemo: React.FC = () => {
const [isTracking, setIsTracking] = useState(false);
const [currentLocation, setCurrentLocation] = useState<LocationData | null>(
null
);
const [geofences, setGeofences] = useState<Geofence[]>([]);
const [geofenceEvents, setGeofenceEvents] = useState<GeofenceEvent[]>([]);
const [newGeofence, setNewGeofence] = useState({
title: '',
radius: '100',
});
useEffect(() => {
initializeServices();
return () => {
GeofenceService.stopMonitoring();
LocationService.stopTracking();
};
}, []);
const initializeServices = async () => {
// Configure background tasks
try {
await BackgroundTaskService.configure({
minimumFetchInterval: 15,
stopOnTerminate: false,
startOnBoot: true,
});
// Register a background task for geofence checking
BackgroundTaskService.registerTask('geofence-check', async () => {
console.log('Background geofence check');
// You can add additional logic here
});
} catch (error) {
console.error('Failed to initialize background tasks:', error);
}
};
const requestPermissions = async () => {
const granted = await PermissionHelper.requestLocationPermission('always');
if (granted) {
Alert.alert('Success', 'Location permissions granted');
} else {
Alert.alert('Error', 'Location permissions denied');
}
};
const startTracking = async () => {
const hasPermission = await PermissionHelper.hasLocationPermission();
if (!hasPermission) {
Alert.alert(
'Permission Required',
'Please grant location permission first'
);
return;
}
LocationService.startTracking(
(location) => {
setCurrentLocation(location);
},
(error) => {
Alert.alert('Error', 'Failed to track location: ' + error.message);
}
);
GeofenceService.startMonitoring((event) => {
handleGeofenceEvent(event);
});
setIsTracking(true);
};
const stopTracking = () => {
LocationService.stopTracking();
GeofenceService.stopMonitoring();
setIsTracking(false);
};
const handleGeofenceEvent = (event: GeofenceEvent) => {
setGeofenceEvents((prev) => [event, ...prev.slice(0, 9)]); // Keep last 10 events
// Show alert for geofence events
Alert.alert(
`Geofence ${event.type}`,
`You ${event.type === 'enter' ? 'entered' : 'exited'} ${
event.geofence.title || event.geofence.id
}`
);
// You can trigger other actions here:
// - Send notification
// - Update server
// - Change app state
// - Log analytics
};
const addGeofence = async () => {
if (!currentLocation) {
Alert.alert('Error', 'Current location not available');
return;
}
if (!newGeofence.title) {
Alert.alert('Error', 'Please enter a title for the geofence');
return;
}
const radius = parseInt(newGeofence.radius);
if (isNaN(radius) || radius < 10) {
Alert.alert('Error', 'Radius must be at least 10 meters');
return;
}
const geofence: Geofence = {
id: `geofence_${Date.now()}`,
latitude: currentLocation.latitude,
longitude: currentLocation.longitude,
radius,
title: newGeofence.title,
description: `Created at ${new Date().toLocaleString()}`,
};
GeofenceService.addGeofence(geofence);
setGeofences([...GeofenceService.getGeofences()]);
setNewGeofence({ title: '', radius: '100' });
Alert.alert('Success', `Geofence "${geofence.title}" added`);
};
const removeGeofence = (geofenceId: string) => {
GeofenceService.removeGeofence(geofenceId);
setGeofences([...GeofenceService.getGeofences()]);
};
const getCurrentLocation = async () => {
try {
const location = await LocationService.getCurrentLocation();
setCurrentLocation(location);
Alert.alert(
'Current Location',
`Lat: ${location.latitude.toFixed(6)}\nLon: ${location.longitude.toFixed(
6
)}\nAccuracy: ${location.accuracy.toFixed(2)}m`
);
} catch (error: any) {
Alert.alert('Error', 'Failed to get location: ' + error.message);
}
};
const renderGeofence = ({ item }: { item: Geofence }) => {
const isActive = GeofenceService.isInsideGeofence(item.id);
const distance = GeofenceService.getDistanceToGeofence(item.id);
return (
<View style={[styles.geofenceItem, isActive && styles.activeGeofence]}>
<View style={styles.geofenceHeader}>
<Text style={styles.geofenceTitle}>{item.title}</Text>
<TouchableOpacity onPress={() => removeGeofence(item.id)}>
<Text style={styles.removeButton}>Remove</Text>
</TouchableOpacity>
</View>
<Text style={styles.geofenceDetail}>Radius: {item.radius}m</Text>
<Text style={styles.geofenceDetail}>
Status: {isActive ? '🟢 Inside' : '⭕ Outside'}
</Text>
{distance !== null && (
<Text style={styles.geofenceDetail}>
Distance: {distance.toFixed(0)}m
</Text>
)}
{item.description && (
<Text style={styles.geofenceDescription}>{item.description}</Text>
)}
</View>
);
};
const renderEvent = ({ item }: { item: GeofenceEvent }) => (
<View style={styles.eventItem}>
<Text style={styles.eventTitle}>
{item.type === 'enter' ? '📍' : '🚪'} {item.geofence.title}
</Text>
<Text style={styles.eventDetail}>
{item.type.toUpperCase()} at {new Date(item.timestamp).toLocaleTimeString()}
</Text>
</View>
);
return (
<ScrollView style={styles.container}>
<Text style={styles.header}>Geofencing Demo</Text>
{/* Permissions */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Permissions</Text>
<TouchableOpacity
style={styles.button}
onPress={requestPermissions}
>
<Text style={styles.buttonText}>Request Location Permissions</Text>
</TouchableOpacity>
</View>
{/* Location Tracking */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Location Tracking</Text>
<View style={styles.trackingRow}>
<Text>Background Tracking:</Text>
<Switch value={isTracking} onValueChange={isTracking ? stopTracking : startTracking} />
</View>
<TouchableOpacity
style={styles.button}
onPress={getCurrentLocation}
>
<Text style={styles.buttonText}>Get Current Location</Text>
</TouchableOpacity>
{currentLocation && (
<View style={styles.locationInfo}>
<Text style={styles.infoText}>
📍 Latitude: {currentLocation.latitude.toFixed(6)}
</Text>
<Text style={styles.infoText}>
📍 Longitude: {currentLocation.longitude.toFixed(6)}
</Text>
<Text style={styles.infoText}>
🎯 Accuracy: {currentLocation.accuracy.toFixed(2)}m
</Text>
</View>
)}
</View>
{/* Add Geofence */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Add Geofence</Text>
<TextInput
style={styles.input}
placeholder="Geofence Title"
value={newGeofence.title}
onChangeText={(text) =>
setNewGeofence({ ...newGeofence, title: text })
}
/>
<TextInput
style={styles.input}
placeholder="Radius (meters)"
keyboardType="numeric"
value={newGeofence.radius}
onChangeText={(text) =>
setNewGeofence({ ...newGeofence, radius: text })
}
/>
<TouchableOpacity
style={[styles.button, !currentLocation && styles.buttonDisabled]}
onPress={addGeofence}
disabled={!currentLocation}
>
<Text style={styles.buttonText}>
Add Geofence at Current Location
</Text>
</TouchableOpacity>
</View>
{/* Active Geofences */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Geofences ({geofences.length})
</Text>
{geofences.length === 0 ? (
<Text style={styles.emptyText}>No geofences yet</Text>
) : (
<FlatList
data={geofences}
renderItem={renderGeofence}
keyExtractor={(item) => item.id}
scrollEnabled={false}
/>
)}
</View>
{/* Recent Events */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Recent Events ({geofenceEvents.length})
</Text>
{geofenceEvents.length === 0 ? (
<Text style={styles.emptyText}>No events yet</Text>
) : (
<FlatList
data={geofenceEvents}
renderItem={renderEvent}
keyExtractor={(item, index) => `${item.geofence.id}_${index}`}
scrollEnabled={false}
/>
)}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
fontSize: 24,
fontWeight: 'bold',
padding: 20,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
section: {
backgroundColor: '#fff',
padding: 15,
marginTop: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: '#e0e0e0',
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
color: '#333',
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 10,
},
buttonDisabled: {
backgroundColor: '#ccc',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
trackingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10,
},
locationInfo: {
marginTop: 15,
padding: 10,
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
infoText: {
fontSize: 14,
marginBottom: 5,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 12,
borderRadius: 8,
marginBottom: 10,
fontSize: 16,
},
geofenceItem: {
padding: 12,
backgroundColor: '#f9f9f9',
borderRadius: 8,
marginBottom: 10,
borderWidth: 1,
borderColor: '#e0e0e0',
},
activeGeofence: {
backgroundColor: '#e8f5e9',
borderColor: '#4caf50',
borderWidth: 2,
},
geofenceHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 5,
},
geofenceTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
removeButton: {
color: '#f44336',
fontSize: 14,
fontWeight: '600',
},
geofenceDetail: {
fontSize: 14,
color: '#666',
marginBottom: 2,
},
geofenceDescription: {
fontSize: 12,
color: '#999',
marginTop: 5,
fontStyle: 'italic',
},
eventItem: {
padding: 10,
backgroundColor: '#fff3e0',
borderRadius: 8,
marginBottom: 8,
borderLeftWidth: 3,
borderLeftColor: '#ff9800',
},
eventTitle: {
fontSize: 15,
fontWeight: '600',
color: '#333',
marginBottom: 3,
},
eventDetail: {
fontSize: 13,
color: '#666',
},
emptyText: {
fontSize: 14,
color: '#999',
fontStyle: 'italic',
textAlign: 'center',
paddingVertical: 20,
},
});
export default GeofenceDemo;
Using the Demo in Your App
Update your App.tsx
to use the demo screen:
import React from 'react';
import { SafeAreaView, StatusBar, StyleSheet } from 'react-native';
import GeofenceDemo from './screens/GeofenceDemo';
const App = () => {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<GeofenceDemo />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default App;
Testing Your Geofencing App
Testing on iOS
- Build and run the app on a physical device (geofencing doesn't work well on simulators)
- Grant permissions when prompted
- Create a geofence at your current location
- Walk away from the location (at least beyond the radius you set)
- Observe notifications when you exit the geofence
- Walk back to see the entry notification
You can also simulate location changes in Xcode:
- Debug → Simulate Location → Custom Location
Testing on Android
- Enable Developer Options on your device
- Enable "Allow mock locations"
- Build and run the app
- Grant permissions including background location
- Use apps like "Fake GPS Location" to simulate movement
- Test geofence events by changing your mock location
Testing Background Behavior
To test if your app works in the background:
- Start tracking and create geofences
- Lock your device or minimize the app
- Move to different locations (or simulate movement)
- Check logs using:
# iOS npx react-native log-ios # Android npx react-native log-android
Best Practices
1. Battery Optimization
- Use appropriate accuracy levels: Don't always use high accuracy
- Increase distance filter: Only update on significant movement
- Limit number of geofences: Monitor only essential geofences (iOS allows max 20)
- Use significant location changes: For less critical tracking
// Battery-friendly configuration
LocationService.startTracking(
onLocation,
onError,
{
distanceFilter: 50, // Update every 50 meters instead of 10
desiredAccuracy: {
ios: Geolocation.ACCURACY_BALANCED,
android: Geolocation.ACCURACY_BALANCED,
},
interval: 30000, // 30 seconds
}
);
2. Privacy and Transparency
- Explain why you need location access
- Request permissions at the right time (not immediately on app launch)
- Provide value first before asking for permissions
- Allow users to control tracking settings
- Follow platform guidelines (App Store and Play Store requirements)
3. Error Handling
Always handle edge cases:
// Robust error handling
try {
const hasPermission = await PermissionHelper.hasLocationPermission();
if (!hasPermission) {
// Handle missing permission
return;
}
const location = await LocationService.getCurrentLocation();
// Use location
} catch (error: any) {
if (error.code === 'PERMISSION_DENIED') {
// Handle permission denied
} else if (error.code === 'TIMEOUT') {
// Handle timeout
} else {
// Handle other errors
}
}
4. Offline Support
Implement data persistence for offline scenarios:
import AsyncStorage from '@react-native-async-storage/async-storage';
// Save geofence events locally
async function saveGeofenceEvent(event: GeofenceEvent) {
try {
const events = await AsyncStorage.getItem('geofence_events');
const eventList = events ? JSON.parse(events) : [];
eventList.push(event);
await AsyncStorage.setItem('geofence_events', JSON.stringify(eventList));
} catch (error) {
console.error('Failed to save event:', error);
}
}
5. Platform Differences
Be aware of platform-specific behaviors:
- iOS: Limited to 20 geofences, requires "Always" permission for background
- Android: Different behavior on Android 10+ for background location
- Battery optimization: Android devices may restrict background tasks
- Permission dialogs: Different UX on each platform
Advanced Features
Push Notifications for Geofence Events
Integrate push notifications to alert users about geofence events:
import PushNotification from 'react-native-push-notification';
// Configure push notifications
PushNotification.configure({
onNotification: function (notification) {
console.log('Notification:', notification);
},
permissions: {
alert: true,
badge: true,
sound: true,
},
popInitialNotification: true,
requestPermissions: true,
});
// Show notification on geofence event
function showGeofenceNotification(event: GeofenceEvent) {
PushNotification.localNotification({
title: `Geofence ${event.type}`,
message: `You ${event.type}ed ${event.geofence.title}`,
playSound: true,
soundName: 'default',
});
}
Server Integration
Send geofence events to your backend:
async function syncGeofenceEvent(event: GeofenceEvent) {
try {
const response = await fetch('https://your-api.com/geofence-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${yourAuthToken}`,
},
body: JSON.stringify({
geofenceId: event.geofence.id,
eventType: event.type,
timestamp: event.timestamp,
location: {
latitude: event.location.latitude,
longitude: event.location.longitude,
},
}),
});
if (!response.ok) {
throw new Error('Failed to sync event');
}
console.log('Event synced successfully');
} catch (error) {
console.error('Sync error:', error);
// Queue for retry
}
}
Analytics Tracking
Track geofence usage and patterns:
import analytics from '@react-native-firebase/analytics';
async function logGeofenceEvent(event: GeofenceEvent) {
await analytics().logEvent('geofence_event', {
geofence_id: event.geofence.id,
event_type: event.type,
geofence_title: event.geofence.title,
timestamp: event.timestamp,
});
}
Troubleshooting Common Issues
Issue 1: Location Not Updating in Background
Solution:
- Ensure background modes are enabled in Xcode
- Check that "Always" permission is granted
- Verify AndroidManifest.xml has correct permissions
- Check device battery optimization settings
Issue 2: Geofence Events Not Triggering
Solution:
- Ensure you're testing on a physical device
- Check that the device has moved beyond the geofence radius
- Verify location accuracy is sufficient
- Check logs for error messages
Issue 3: App Crashes on Permission Request
Solution:
- Verify Info.plist has all required usage descriptions
- Check AndroidManifest.xml has correct permissions
- Test permission flow separately before integrating
Issue 4: High Battery Drain
Solution:
- Increase distance filter
- Reduce location accuracy
- Limit number of active geofences
- Use significant location changes instead of continuous tracking
Issue 5: Android Background Restrictions
Solution:
- Request to be excluded from battery optimization
- Use foreground service for critical tracking
- Educate users about enabling background permissions
Performance Optimization
Memory Management
// Clean up properly
useEffect(() => {
return () => {
// Stop all tracking when component unmounts
GeofenceService.stopMonitoring();
LocationService.stopTracking();
BackgroundTaskService.stop();
};
}, []);
Debouncing Location Updates
import { debounce } from 'lodash';
// Debounce location updates to reduce processing
const debouncedLocationUpdate = debounce((location: LocationData) => {
// Process location
updateUI(location);
}, 1000); // Update UI at most once per second
Lazy Loading Geofences
For apps with many geofences, only monitor nearby ones:
function getRelevantGeofences(
currentLocation: LocationData,
allGeofences: Geofence[],
maxDistance: number = 5000 // 5km
): Geofence[] {
return allGeofences.filter((geofence) => {
const distance = LocationService.calculateDistance(
currentLocation.latitude,
currentLocation.longitude,
geofence.latitude,
geofence.longitude
);
return distance <= maxDistance;
});
}
Conclusion
You now have a complete implementation of background tasks and geofencing in React Native! This powerful combination enables you to build location-aware applications that provide contextual experiences based on where your users are.
Key takeaways:
- Always request permissions thoughtfully and explain why you need them
- Optimize for battery life by using appropriate accuracy and distance filters
- Test thoroughly on physical devices to ensure reliability
- Handle edge cases and offline scenarios gracefully
- Respect user privacy and follow platform guidelines
- Monitor performance and adjust settings based on your specific use case
The techniques covered in this guide can be applied to various applications:
- Ride-sharing and delivery apps
- Fitness and health tracking
- Smart home automation
- Retail and proximity marketing
- Time and attendance systems
- Field service management
Remember that with great power comes great responsibility. Location tracking and background tasks should enhance user experience, not compromise privacy or drain battery life. Always put user trust and experience first.
Happy coding, and may your geofences always trigger at the right time! 🎯