@gabriel-sisjr/react-native-background-location
Version:
React Native library for background location tracking using TurboModules. Track user location even when the app is minimized or in the background.
280 lines (250 loc) • 7.99 kB
text/typescript
import { useState, useEffect, useCallback, useRef } from 'react';
import { NativeEventEmitter } from 'react-native';
import BackgroundLocationModule from '../NativeBackgroundLocation';
import type {
UseLocationUpdatesOptions,
UseLocationUpdatesResult,
Coords,
LocationUpdateEvent,
} from '../types';
import { extractDefinedProperties } from '../utils/objectUtils';
// Check if native module is available
const isNativeModuleAvailable = () => {
try {
// Check if methods are available (works with Proxy mocks)
// This must be checked first before checking if module exists
if (typeof BackgroundLocationModule?.isTracking !== 'function') {
return false;
}
// Check if module exists and is not null
if (!BackgroundLocationModule || BackgroundLocationModule === null) {
return false;
}
return true;
} catch {
return false;
}
};
/**
* Hook to watch location updates in real-time
*
* This hook automatically listens for location updates from the background service
* and provides them as they arrive, without requiring manual refresh.
*
* @param options - Configuration options
*
* @example
* ```tsx
* function LiveTrackingMap() {
* const {
* locations,
* lastLocation,
* isTracking
* } = useLocationUpdates({
* onLocationUpdate: (location) => {
* console.log('New location:', location);
* },
* });
*
* return (
* <View>
* <Text>Tracking: {isTracking ? 'Active' : 'Inactive'}</Text>
* <Text>Locations collected: {locations.length}</Text>
* {lastLocation && (
* <Text>
* Last: {lastLocation.latitude}, {lastLocation.longitude}
* </Text>
* )}
* </View>
* );
* }
* ```
*/
export function useLocationUpdates(
options: UseLocationUpdatesOptions = {}
): UseLocationUpdatesResult {
const { tripId: providedTripId, onLocationUpdate, autoLoad = true } = options;
const [tripId, setTripId] = useState<string | null>(providedTripId || null);
const [isTracking, setIsTracking] = useState(false);
const [locations, setLocations] = useState<Coords[]>([]);
const [lastLocation, setLastLocation] = useState<Coords | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const wasClearedRef = useRef(false);
/**
* Clear error state
*/
const clearError = useCallback(() => {
setError(null);
}, []);
/**
* Clear all locations for current trip
*/
const clearLocations = useCallback(async (): Promise<void> => {
if (!tripId) {
return;
}
if (!isNativeModuleAvailable()) {
setLocations([]);
setLastLocation(null);
wasClearedRef.current = true;
console.warn('BackgroundLocation not available');
return;
}
setIsLoading(true);
clearError();
try {
await BackgroundLocationModule.clearTrip(tripId);
setLocations([]);
setLastLocation(null);
wasClearedRef.current = true;
} catch (err) {
const clearError_instance =
err instanceof Error ? err : new Error('Failed to clear locations');
setError(clearError_instance);
console.error('Error clearing locations:', clearError_instance);
} finally {
setIsLoading(false);
}
}, [tripId, clearError]);
/**
* Load existing locations for a trip
*/
const loadExistingLocations = useCallback(
async (loadTripId: string) => {
if (!isNativeModuleAvailable()) {
return;
}
setIsLoading(true);
clearError();
try {
const locs = await BackgroundLocationModule.getLocations(loadTripId);
setLocations(locs);
if (locs.length > 0) {
const lastLoc = locs[locs.length - 1];
if (lastLoc) {
setLastLocation(lastLoc);
}
}
} catch (err) {
const loadError =
err instanceof Error
? err
: new Error('Failed to load existing locations');
setError(loadError);
console.error('Error loading locations:', loadError);
} finally {
setIsLoading(false);
}
},
[clearError]
);
/**
* Check tracking status and setup initial state
*/
useEffect(() => {
const checkStatus = async () => {
if (!isNativeModuleAvailable()) {
console.warn(
'BackgroundLocation not available - running in simulator or module not linked?'
);
return;
}
try {
const status = await BackgroundLocationModule.isTracking();
setIsTracking(status.active);
// If we have a provided tripId, use it; otherwise use the active one
const effectiveTripId = providedTripId || status.tripId;
if (effectiveTripId) {
setTripId(effectiveTripId);
// Load existing locations if autoLoad is enabled and wasn't recently cleared
if (autoLoad && !wasClearedRef.current) {
await loadExistingLocations(effectiveTripId);
} else if (wasClearedRef.current) {
// Reset the cleared flag after a delay to allow reloading on next check
setTimeout(() => {
wasClearedRef.current = false;
}, 2000);
}
}
} catch (err) {
console.error('Error checking tracking status:', err);
}
};
checkStatus();
// Re-check status periodically (every 5 seconds) to catch tracking changes
const interval = setInterval(() => {
checkStatus();
}, 5000);
return () => {
clearInterval(interval);
};
}, [providedTripId, autoLoad, loadExistingLocations]);
/**
* Listen for location update events
*/
useEffect(() => {
if (!isNativeModuleAvailable()) {
return;
}
// Create event emitter without passing the module (for TurboModule compatibility)
// The native module emits events via DeviceEventManagerModule
const eventEmitter = new NativeEventEmitter();
const subscription = eventEmitter.addListener(
'onLocationUpdate',
(event: any) => {
const locationEvent = event as LocationUpdateEvent;
// Only process events for the trip we're watching (or all if no specific trip)
if (!tripId || locationEvent.tripId === tripId) {
// Extract all defined properties (both required and optional)
const newLocation = extractDefinedProperties(locationEvent) as Coords;
// Update trip ID if we weren't watching a specific one
if (!tripId && locationEvent.tripId) {
setTripId(locationEvent.tripId);
setIsTracking(true);
}
// If locations were cleared, start fresh (don't append to empty array)
// Otherwise, add to existing locations array
setLocations((prev) => {
// If was cleared, start fresh with just the new location
// Otherwise, append to existing array
if (wasClearedRef.current && prev.length === 0) {
wasClearedRef.current = false; // Reset cleared flag when new location arrives
return [newLocation];
}
return [...prev, newLocation];
});
setLastLocation(newLocation);
// Call callback if provided
onLocationUpdate?.(newLocation);
}
}
);
return () => {
subscription.remove();
};
}, [tripId, onLocationUpdate]);
/**
* Reset state when trip changes
*/
useEffect(() => {
if (providedTripId && providedTripId !== tripId) {
setTripId(providedTripId);
setLocations([]);
setLastLocation(null);
if (autoLoad) {
loadExistingLocations(providedTripId);
}
}
}, [providedTripId, tripId, autoLoad, loadExistingLocations]);
return {
tripId,
isTracking,
locations,
lastLocation,
isLoading,
error,
clearError,
clearLocations,
};
}