By the end of this lesson, you will:
expo-locationGeofencing creates invisible boundaries that trigger actions when users enter or exit. Apps like Starbucks (rewards notifications), Life360 (family location sharing), and Pokemon GO (spawn points) use geofences to create context-aware experiences.
The magic of geofencing is anticipation. Your app knows when users arrive at important places before they even open it. This creates "wow" moments that users share: "How did it know I was here?"
A geofence is a virtual perimeter around a geographic location. When a device enters or exits this boundary, your app receives a notification.
Key Components:
Common Use Cases:
Geofencing requires location permissions, often including background access:
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
import { Alert } from 'react-native';
export default function GeofenceSetup() {
const [hasPermissions, setHasPermissions] = useState(false);
useEffect(() => {
requestPermissions();
}, []);
const requestPermissions = async () => {
// Request foreground location
const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync();
if (foregroundStatus !== 'granted') {
Alert.alert('Permission Denied', 'Location access is required for geofencing');
return;
}
// Request background location (critical for geofencing)
const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync();
if (backgroundStatus !== 'granted') {
Alert.alert(
'Background Location Required',
'For geofencing to work when the app is closed, please enable "Always" location access in settings.'
);
return;
}
setHasPermissions(true);
};
return hasPermissions;
}
💡 Tip: Always explain WHY you need background location. Users are more likely to grant permission when they understand the value.
Define a circular geofence with expo-location:
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
const GEOFENCE_TASK_NAME = 'GEOFENCE_TASK';
// Define the background task
TaskManager.defineTask(GEOFENCE_TASK_NAME, ({ data: { eventType, region }, error }) => {
if (error) {
console.error('Geofence task error:', error);
return;
}
if (eventType === Location.GeofencingEventType.Enter) {
console.log("You've entered the region:", region);
} else if (eventType === Location.GeofencingEventType.Exit) {
console.log("You've left the region:", region);
}
});
export const startGeofencing = async () => {
// Check if task is already running
const hasStarted = await Location.hasStartedGeofencingAsync(GEOFENCE_TASK_NAME);
if (hasStarted) {
console.log('Geofencing already active');
return;
}
// Start geofencing
await Location.startGeofencingAsync(GEOFENCE_TASK_NAME, [
{
identifier: 'home',
latitude: 37.78825,
longitude: -122.4324,
radius: 100, // meters
notifyOnEnter: true,
notifyOnExit: true,
},
{
identifier: 'work',
latitude: 37.7749,
longitude: -122.4194,
radius: 50,
notifyOnEnter: true,
notifyOnExit: false, // Only notify on entry
},
]);
console.log('Geofencing started');
};
export const stopGeofencing = async () => {
await Location.stopGeofencingAsync(GEOFENCE_TASK_NAME);
console.log('Geofencing stopped');
};
Geofence Configuration:
identifier: Unique name for the geofencelatitude / longitude: Center coordinatesradius: Trigger distance in meters (recommended: 100-500m)notifyOnEnter: Trigger when enteringnotifyOnExit: Trigger when exitingProcess entry/exit events in your background task:
import * as Notifications from 'expo-notifications';
TaskManager.defineTask(GEOFENCE_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error('Geofence error:', error);
return;
}
const { eventType, region } = data;
if (eventType === Location.GeofencingEventType.Enter) {
await handleGeofenceEnter(region);
} else if (eventType === Location.GeofencingEventType.Exit) {
await handleGeofenceExit(region);
}
});
const handleGeofenceEnter = async (region) => {
console.log(`Entered: ${region.identifier}`);
// Send notification
await Notifications.scheduleNotificationAsync({
content: {
title: 'Welcome!',
body: `You've arrived at ${region.identifier}`,
data: { region: region.identifier },
},
trigger: null, // Send immediately
});
// Log to analytics
// logEvent('geofence_enter', { location: region.identifier });
// Update app state (if app is open)
// AppState.setCurrentLocation(region.identifier);
};
const handleGeofenceExit = async (region) => {
console.log(`Exited: ${region.identifier}`);
await Notifications.scheduleNotificationAsync({
content: {
title: 'Goodbye!',
body: `You've left ${region.identifier}`,
},
trigger: null,
});
};
Add/remove geofences based on user actions:
import { useState } from 'react';
import * as Location from 'expo-location';
export default function DynamicGeofenceManager() {
const [activeGeofences, setActiveGeofences] = useState([]);
const addGeofence = async (place) => {
const newGeofences = [
...activeGeofences,
{
identifier: place.id,
latitude: place.coordinate.latitude,
longitude: place.coordinate.longitude,
radius: 200,
notifyOnEnter: true,
notifyOnExit: true,
},
];
await Location.startGeofencingAsync(GEOFENCE_TASK_NAME, newGeofences);
setActiveGeofences(newGeofences);
console.log(`Added geofence: ${place.name}`);
};
const removeGeofence = async (placeId) => {
const updatedGeofences = activeGeofences.filter(
(geofence) => geofence.identifier !== placeId
);
await Location.startGeofencingAsync(GEOFENCE_TASK_NAME, updatedGeofences);
setActiveGeofences(updatedGeofences);
console.log(`Removed geofence: ${placeId}`);
};
const clearAllGeofences = async () => {
await Location.stopGeofencingAsync(GEOFENCE_TASK_NAME);
setActiveGeofences([]);
console.log('All geofences cleared');
};
return {
activeGeofences,
addGeofence,
removeGeofence,
clearAllGeofences,
};
}
For continuous location tracking (alternative to geofencing):
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
const LOCATION_TASK_NAME = 'BACKGROUND_LOCATION_TASK';
TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
if (error) {
console.error('Location task error:', error);
return;
}
if (data) {
const { locations } = data;
console.log('Background location update:', locations);
// Process location updates
// checkProximityToGeofences(locations[0]);
}
});
export const startBackgroundLocationTracking = async () => {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
accuracy: Location.Accuracy.Balanced,
timeInterval: 10000, // Update every 10 seconds
distanceInterval: 50, // Or when moved 50 meters
foregroundService: {
notificationTitle: 'Tracking Location',
notificationBody: 'Your location is being monitored',
},
});
};
export const stopBackgroundLocationTracking = async () => {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
};
Background Tracking Options:
accuracy: Balanced, High, Low, LowesttimeInterval: Minimum milliseconds between updatesdistanceInterval: Minimum meters between updatesforegroundService: Required notification for Android💡 Tip: Use
Balancedaccuracy for geofencing to save battery. High accuracy drains battery quickly.
Minimize battery drain while maintaining functionality:
// Strategy 1: Adaptive Radius
const getAdaptiveRadius = (locationType) => {
switch (locationType) {
case 'home':
return 100; // Precise for home
case 'store':
return 50; // Very precise for retail
case 'city':
return 5000; // Broad for large areas
default:
return 200;
}
};
// Strategy 2: Time-Based Activation
const shouldActivateGeofence = (schedule) => {
const now = new Date();
const hour = now.getHours();
// Only monitor coffee shop geofence during morning hours
if (schedule.type === 'coffee' && (hour < 7 || hour > 11)) {
return false;
}
return true;
};
// Strategy 3: Limit Active Geofences
const MAX_ACTIVE_GEOFENCES = 20; // iOS limit
const prioritizeGeofences = (allGeofences, userLocation) => {
// Sort by distance from user
return allGeofences
.sort((a, b) => {
const distA = calculateDistance(userLocation, a.coordinate);
const distB = calculateDistance(userLocation, b.coordinate);
return distA - distB;
})
.slice(0, MAX_ACTIVE_GEOFENCES);
};
Test geofences without physical movement:
// Development mode: Simulate location changes
import * as Location from 'expo-location';
const simulateGeofenceEntry = async (geofenceId) => {
// In development, manually trigger events
if (__DEV__) {
console.log(`[DEBUG] Simulating entry to ${geofenceId}`);
// Trigger your handler directly
await handleGeofenceEnter({ identifier: geofenceId });
}
};
// Check geofencing status
const debugGeofencing = async () => {
const hasStarted = await Location.hasStartedGeofencingAsync(GEOFENCE_TASK_NAME);
console.log('Geofencing active:', hasStarted);
const { status } = await Location.getBackgroundPermissionsAsync();
console.log('Background permission:', status);
// Get current location
const location = await Location.getCurrentPositionAsync({});
console.log('Current location:', location.coords);
};
Complete geofencing system with notification support:
import { useState, useEffect } from 'react';
import { View, Text, Button, FlatList, Alert, StyleSheet } from 'react-native';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
import * as TaskManager from 'expo-task-manager';
const GEOFENCE_TASK_NAME = 'GEOFENCE_MONITORING';
// Configure notifications
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
// Define geofence task
TaskManager.defineTask(GEOFENCE_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error('Geofence task error:', error);
return;
}
const { eventType, region } = data;
if (eventType === Location.GeofencingEventType.Enter) {
await Notifications.scheduleNotificationAsync({
content: {
title: 'Location Alert',
body: `You entered ${region.identifier}`,
sound: true,
},
trigger: null,
});
}
});
export default function GeofenceManagerScreen() {
const [geofences, setGeofences] = useState([
{
id: 'home',
name: 'Home',
latitude: 37.78825,
longitude: -122.4324,
radius: 100,
active: false,
},
{
id: 'work',
name: 'Work',
latitude: 37.7749,
longitude: -122.4194,
radius: 150,
active: false,
},
]);
useEffect(() => {
setupPermissions();
}, []);
const setupPermissions = async () => {
const { status: notifStatus } = await Notifications.requestPermissionsAsync();
if (notifStatus !== 'granted') {
Alert.alert('Notification permission required');
return;
}
const { status: fgStatus } = await Location.requestForegroundPermissionsAsync();
if (fgStatus !== 'granted') {
Alert.alert('Location permission required');
return;
}
const { status: bgStatus } = await Location.requestBackgroundPermissionsAsync();
if (bgStatus !== 'granted') {
Alert.alert('Background location recommended for best experience');
}
};
const startMonitoring = async () => {
const activeGeofences = geofences.map((geofence) => ({
identifier: geofence.id,
latitude: geofence.latitude,
longitude: geofence.longitude,
radius: geofence.radius,
notifyOnEnter: true,
notifyOnExit: true,
}));
try {
await Location.startGeofencingAsync(GEOFENCE_TASK_NAME, activeGeofences);
setGeofences(geofences.map((g) => ({ ...g, active: true })));
Alert.alert('Success', 'Geofencing started');
} catch (error) {
Alert.alert('Error', error.message);
}
};
const stopMonitoring = async () => {
try {
await Location.stopGeofencingAsync(GEOFENCE_TASK_NAME);
setGeofences(geofences.map((g) => ({ ...g, active: false })));
Alert.alert('Success', 'Geofencing stopped');
} catch (error) {
Alert.alert('Error', error.message);
}
};
const renderGeofence = ({ item }) => (
<View style={styles.geofenceItem}>
<View>
<Text style={styles.geofenceName}>{item.name}</Text>
<Text style={styles.geofenceDetails}>
Radius: {item.radius}m | Status: {item.active ? '🟢 Active' : '⚪ Inactive'}
</Text>
</View>
</View>
);
return (
<View style={styles.container}>
<Text style={styles.title}>Geofence Manager</Text>
<FlatList
data={geofences}
renderItem={renderGeofence}
keyExtractor={(item) => item.id}
style={styles.list}
/>
<View style={styles.controls}>
<Button title="Start Monitoring" onPress={startMonitoring} />
<Button title="Stop Monitoring" onPress={stopMonitoring} color="#ff3b30" />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
},
list: {
flex: 1,
},
geofenceItem: {
backgroundColor: '#f5f5f5',
padding: 16,
borderRadius: 8,
marginBottom: 12,
},
geofenceName: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 4,
},
geofenceDetails: {
fontSize: 14,
color: '#666',
},
controls: {
gap: 12,
marginTop: 20,
},
});
| Pitfall | Solution |
|---|---|
| Geofences not triggering | Ensure background permissions granted, test with larger radius first |
| Battery drain | Use Balanced accuracy, limit active geofences, increase radius |
| Delayed notifications | iOS/Android may delay background tasks to save battery, expected behavior |
| Too many geofences | Limit to 20 active geofences (iOS limit), prioritize by proximity |
| Not working when app killed | Verify background permissions and foreground service notification |
expo-location and TaskManager for geofence monitoringIn the next lesson, we'll explore ARCore Fundamentals, diving into augmented reality to overlay digital content on the physical world through your device's camera.