How to 'setState' from within TaskManager

OPTION 1 - PUBLISHER / SUBSCRIBER PATTERN
This is a simple example in which latitude and longitude are tracked in the background using the publisher/subscriber pattern. A summary of what is happening:

  1. The LocationService function has internal state through a closure. It returns an object with several methods on it. The important one to get your head around is subscribe. When this is called it is passed a reference to a function as an argument, and that reference is added to the subscribers array.
  2. componentDidMount calls locationService.subscribe(this.onLocationUpdate) passing the class method onLocationUpdate into the subscribers array.
  3. TaskManager executes its callback each time new data is received. The callback contains a call to LocationService.setLocation({latitude, longitude}). This takes the latLng object and passes it to every function contained in the subscribers array. This includes the class method onLocationUpdate and therefore the class state is updated, which consequently re-renders the component.
/* LOCATION SERVICE - PUB/SUB SOLUTION */

/* PUBLISHER */
const LocationService = () => {
  let subscribers = []
  let location = {
    latitude: 0,
    longitude: 0
  }

  return {
    subscribe: (sub) => subscribers.push(sub),
    setLocation: (coords) => {
      location = coords
      subscribers.forEach((sub) => sub(location))
    },
    unsubscribe: (sub) => {
      subscribers = subscribers.filter((_sub) => _sub !== sub)
    }
  }
}

export const locationService = LocationService()

/* SUBSCRIBER */

import * as Location from 'expo-location'
import * as Permissions from 'expo-permissions'
import * as TaskManager from 'expo-task-manager'
import React from 'react'
import { Text, View } from 'react-native'
import { locationService } from './src/locationService'

const LOCATION_TASK_NAME = 'background-location-task'

export default class App extends React.Component {
  state = {
    latitude: 0,
    longitude: 0,
  }

  onLocationUpdate = ({ latitude, longitude }) => {
    this.setState({
      latitude: latitude,
      longitude: longitude
    })
  }

  async componentDidMount() {
    locationService.subscribe(this.onLocationUpdate)
    const { status } = await Permissions.askAsync(
      Permissions.LOCATION,
      Permissions.USER_FACING_NOTIFICATIONS
    )

    if (status === 'granted') {
      await Location.startLocationUpdatesAsync(
        LOCATION_TASK_NAME,
        {
          accuracy: Location.Accuracy.Highest,
          distanceInterval: 1,
          timeInterval: 5000
        }
      )
    }
  }

  componentWillUnmount() {
    locationService.unsubscribe(this.onLocationUpdate)
  }

  render() {
    const { latitude, longitude } = this.state
    return (
      <View>
        <Text>Lat: {latitude}</Text>
        <Text>Lng: {longitude}</Text>
      </View>
    )
  }
}

TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
  if (error) {
    return
  }
  if (data) {
    const { latitude, longitude } = data.locations[0].coords
    locationService.setLocation({
      latitude,
      longitude
    })
  }
})
2 Likes