title: The best way of tracking location in background using react-native + Expo in 2020
body: <p>I want to create my own Endomono/Runtastic-like app using RN + expo (This app will be just for me, and I have android phone with pretty decent performance/battery life (Redmi note 7) so I don't worry about performance too much). I wanted to use all-in-one library for that, or just and library that allows me to execute some code each X seconds in background (and getAsyncLocation there). My point is just to send lat/lon data every X seconds to my backend HTTP django-rest-framework powered server.</p>
  
  <p>I just spent whole day trying figure out any way to do that, I tried couple of libraries like this ones: <a href="https://github.com/mauron85/react-native-background-geolocation" rel="noreferrer">react-native-background-geolocation</a>, <a href="https://github.com/ocetnik/react-native-background-timer" rel="noreferrer">react-native-background-timer</a>, <a href="https://www.npmjs.com/package/react-native-background-job" rel="noreferrer">react-native-background-job</a> and few more. I followed step by step instalation guide, and I kept getting errors like: <code>null is not an object (evaluating 'RNBackgroundTimer.setTimeout')</code> . </p>
  
  <p>I also tried <a href="https://github.com/expo/expo/blob/f882ffbc52ba910ecacefe264081e7eeb77c9665/home/screens/BackgroundLocationScreen.js?fbclid=IwAR30HiOE8q8MNsgO3cJtxLtWQEnLDrX540E78njKitmtbNf0pI0Ol-7rWPQ" rel="noreferrer">this</a>: I fixed some errors in this code (imports related), it seemed to work, but when I changed my GPS location using Fake GPS, and only one cast of didFocus functions appears in the console. Here's code:</p>
  
  <pre><code>import React from 'react';
  import { EventEmitter } from 'fbemitter';
  import { NavigationEvents } from 'react-navigation';
  import { AppState, AsyncStorage, Platform, StyleSheet, Text, View, Button } from 'react-native';
  import MapView from 'react-native-maps';
  import * as Permissions from 'expo-permissions';
  import * as Location from 'expo-location';
  import * as TaskManager from 'expo-task-manager';
  import { FontAwesome, MaterialIcons } from '@expo/vector-icons';
  
  const STORAGE_KEY = 'expo-home-locations';
  const LOCATION_UPDATES_TASK = 'location-updates';
  
  const locationEventsEmitter = new EventEmitter();
  
  export default class MapScreen extends React.Component {
    static navigationOptions = {
      title: 'Background location',
    };
  
    mapViewRef = React.createRef();
  
    state = {
      accuracy: 4,
      isTracking: false,
      showsBackgroundLocationIndicator: false,
      savedLocations: [],
      initialRegion: null,
      error: null,
    };
  
    didFocus = async () =&gt; {
      console.log("Hello")
      let { status } = await Permissions.askAsync(Permissions.LOCATION);
  
      if (status !== 'granted') {
        AppState.addEventListener('change', this.handleAppStateChange);
        this.setState({
          error:
            'Location permissions are required in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.',
        });
        return;
      } else {
        this.setState({ error: null });
      }
  
      const { coords } = await Location.getCurrentPositionAsync();
      console.log(coords)
      const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_UPDATES_TASK);
      const task = (await TaskManager.getRegisteredTasksAsync()).find(
        ({ taskName }) =&gt; taskName === LOCATION_UPDATES_TASK
      );
      const savedLocations = await getSavedLocations();
      const accuracy = (task &amp;&amp; task.options.accuracy) || this.state.accuracy;
  
      this.eventSubscription = locationEventsEmitter.addListener('update', locations =&gt; {
        this.setState({ savedLocations: locations });
      });
  
      if (!isTracking) {
        alert('Click `Start tracking` to start getting location updates.');
      }
  
      this.setState({
        accuracy,
        isTracking,
        savedLocations,
        initialRegion: {
          latitude: coords.latitude,
          longitude: coords.longitude,
          latitudeDelta: 0.004,
          longitudeDelta: 0.002,
        },
      });
    };
  
    handleAppStateChange = nextAppState =&gt; {
      if (nextAppState !== 'active') {
        return;
      }
  
      if (this.state.initialRegion) {
        AppState.removeEventListener('change', this.handleAppStateChange);
        return;
      }
  
      this.didFocus();
    };
  
    componentWillUnmount() {
      if (this.eventSubscription) {
        this.eventSubscription.remove();
      }
  
      AppState.removeEventListener('change', this.handleAppStateChange);
    }
  
    async startLocationUpdates(accuracy = this.state.accuracy) {
      await Location.startLocationUpdatesAsync(LOCATION_UPDATES_TASK, {
        accuracy,
        showsBackgroundLocationIndicator: this.state.showsBackgroundLocationIndicator,
      });
  
      if (!this.state.isTracking) {
        alert(
          'Now you can send app to the background, go somewhere and come back here! You can even terminate the app and it will be woken up when the new significant location change comes out.'
        );
      }
      this.setState({ isTracking: true });
    }
  
    async stopLocationUpdates() {
      await Location.stopLocationUpdatesAsync(LOCATION_UPDATES_TASK);
      this.setState({ isTracking: false });
    }
  
    clearLocations = async () =&gt; {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([]));
      this.setState({ savedLocations: [] });
    };
  
    toggleTracking = async () =&gt; {
      await AsyncStorage.removeItem(STORAGE_KEY);
  
      if (this.state.isTracking) {
        await this.stopLocationUpdates();
      } else {
        await this.startLocationUpdates();
      }
      this.setState({ savedLocations: [] });
    };
  
    onAccuracyChange = () =&gt; {
      const next = Location.Accuracy[this.state.accuracy + 1];
      const accuracy = next ? Location.Accuracy[next] : Location.Accuracy.Lowest;
  
      this.setState({ accuracy });
  
      if (this.state.isTracking) {
        // Restart background task with the new accuracy.
        this.startLocationUpdates(accuracy);
      }
    };
  
    toggleLocationIndicator = async () =&gt; {
      const showsBackgroundLocationIndicator = !this.state.showsBackgroundLocationIndicator;
  
      this.setState({ showsBackgroundLocationIndicator }, async () =&gt; {
        if (this.state.isTracking) {
          await this.startLocationUpdates();
        }
      });
    };
  
    onCenterMap = async () =&gt; {
      const { coords } = await Location.getCurrentPositionAsync();
      const mapView = this.mapViewRef.current;
  
      if (mapView) {
        mapView.animateToRegion({
          latitude: coords.latitude,
          longitude: coords.longitude,
          latitudeDelta: 0.004,
          longitudeDelta: 0.002,
        });
      }
    };
  
    renderPolyline() {
      const { savedLocations } = this.state;
  
      if (savedLocations.length === 0) {
        return null;
      }
      return (
        &lt;MapView.Polyline
          coordinates={savedLocations}
          strokeWidth={3}
          strokeColor={"black"}
        /&gt;
      );
    }
  
    render() {
      if (this.state.error) {
        return &lt;Text style={styles.errorText}&gt;{this.state.error}&lt;/Text&gt;;
      }
  
      if (!this.state.initialRegion) {
        return &lt;NavigationEvents onDidFocus={this.didFocus} /&gt;;
      }
  
      return (
        &lt;View style={styles.screen}&gt;
          &lt;MapView
            ref={this.mapViewRef}
            style={styles.mapView}
            initialRegion={this.state.initialRegion}
            showsUserLocation&gt;
            {this.renderPolyline()}
          &lt;/MapView&gt;
          &lt;View style={styles.buttons} pointerEvents="box-none"&gt;
            &lt;View style={styles.topButtons}&gt;
              &lt;View style={styles.buttonsColumn}&gt;
                {Platform.OS === 'android' ? null : (
                  &lt;Button style={styles.button} onPress={this.toggleLocationIndicator} title="background/indicator"&gt;
                    &lt;Text&gt;{this.state.showsBackgroundLocationIndicator ? 'Hide' : 'Show'}&lt;/Text&gt;
                    &lt;Text&gt; background &lt;/Text&gt;
                    &lt;FontAwesome name="location-arrow" size={20} color="white" /&gt;
                    &lt;Text&gt; indicator&lt;/Text&gt;
                  &lt;/Button&gt;
                )}
              &lt;/View&gt;
              &lt;View style={styles.buttonsColumn}&gt;
                &lt;Button style={styles.button} onPress={this.onCenterMap} title="my location"&gt;
                  &lt;MaterialIcons name="my-location" size={20} color="white" /&gt;
                &lt;/Button&gt;
              &lt;/View&gt;
            &lt;/View&gt;
  
            &lt;View style={styles.bottomButtons}&gt;
              &lt;Button style={styles.button} onPress={this.clearLocations} title="clear locations"&gt;
                Clear locations
              &lt;/Button&gt;
              &lt;Button style={styles.button} onPress={this.toggleTracking} title="start-stop tracking"&gt;
                {this.state.isTracking ? 'Stop tracking' : 'Start tracking'}
              &lt;/Button&gt;
            &lt;/View&gt;
          &lt;/View&gt;
        &lt;/View&gt;
      );
    }
  }
  
  async function getSavedLocations() {
    try {
      const item = await AsyncStorage.getItem(STORAGE_KEY);
      return item ? JSON.parse(item) : [];
    } catch (e) {
      return [];
    }
  }
  
  if (Platform.OS !== 'android') {
    TaskManager.defineTask(LOCATION_UPDATES_TASK, async ({ data: { locations } }) =&gt; {
      if (locations &amp;&amp; locations.length &gt; 0) {
        const savedLocations = await getSavedLocations();
        const newLocations = locations.map(({ coords }) =&gt; ({
          latitude: coords.latitude,
          longitude: coords.longitude,
        }));
  
        savedLocations.push(...newLocations);
        await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations));
  
        locationEventsEmitter.emit('update', savedLocations);
      }
    });
  }
  
  const styles = StyleSheet.create({
    screen: {
      flex: 1,
    },
    mapView: {
      flex: 1,
    },
    buttons: {
      flex: 1,
      flexDirection: 'column',
      justifyContent: 'space-between',
      padding: 10,
      position: 'absolute',
      top: 0,
      right: 0,
      bottom: 0,
      left: 0,
    },
    topButtons: {
      flexDirection: 'row',
      justifyContent: 'space-between',
    },
    bottomButtons: {
      flexDirection: 'column',
      alignItems: 'flex-end',
    },
    buttonsColumn: {
      flexDirection: 'column',
      alignItems: 'flex-start',
    },
    button: {
      paddingVertical: 5,
      paddingHorizontal: 10,
      marginVertical: 5,
    },
    errorText: {
      fontSize: 15,
      color: 'rgba(0,0,0,0.7)',
      margin: 20,
    },
  });
  </code></pre>
  
  <p>If you know any way to easily complete my target (of sending simple HTTP GET with location from background of Expo + RN app to my DRF backend) please let me know.</p>