How to 'setState' from within TaskManager

Hello all,
I would appreciate any guidance you might have regarding this topic, thank you in advance.

Here is my situation:
I have set up TaskManager to track the location of the device it is running on, which is working great. Where I am struggling is getting the data returned by TaskManager into my App component as state. Here is a simplified example of my issue:

class App extends Component {
  state = {}
  // 'this' does not exist within a static method.
  static updateState = (data) => this.setState(data)
  render = () => { return <div>...</div> }
}
​
TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
  const { locations } = data // How can I get locations into state?
  App.updateState(locations) // Won't work as I can't use 'this' within a static method!
})

The actual file looks like this:

/* eslint-disable react/prop-types */
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import * as Location from 'expo-location'
import * as TaskManager from 'expo-task-manager'
import * as Permissions from 'expo-permissions'

const LOCATION_TASK_NAME = 'background-location-task'

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

  static updateState(latitude, longitude) {
    // updateState receives the data correctly, I just can't 
    // work out how to update the component state with values.
    console.log(latitude, longitude) 
    // this.setState({ latitude, longitude }) // Doesn't work
  }

  notStaticUpdateState(latitude, longitude) {
    // console.log(latitude, longitude) // won't work
  }

  async componentDidMount() {
    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: 1000,
        },
      )
    }
  }

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

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    flex: 1,
    justifyContent: 'center',
  },
})

TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
  if (error) {
    return
  }
  if (data) {
    const { latitude, longitude } = data.locations[0].coords
    App.updateState(latitude, longitude)
    // App.notStaticUpdateState(latitude, longitude) // won't work
  }
})

Bump … anyone having an answer here?
Basically on how to communicate between the background task and the foreground app?

You can either set up a pub/sub module (i can post how to do this if you need it), however I think the better solution is simply to use Redux. Once again, if you are not clear I can link a simple example.

If you wouldn’t mind, I’d be interested in both examples.

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
    })
  }
})
1 Like

OPTION 2 - USING REDUX (RECOMMENDED)

I think this is the preferred option as Redux is industry standard and the data flow is simpler. Be aware that this is not a fully functioning example. I haven’t included setup for the Redux store, reducers or actions as that would confuse the point of this post.

  1. TaskManager fires its callback, passing its data to store.dispatch(setLocation({latitude, longitude}))
  2. The connected App component receives the updates
  3. The _App component is passed the updated props and the component re-renders
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 { store } from './redux/store'
import { connect } from 'react-redux'

const LOCATION_TASK_NAME = 'background-location-task'

const _App = ({ latitude, longitude }) => {
  const trackUserLocation = async () => {
    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
        }
      )
    }
  }

  useEffect(() => {
    trackUserLocation() // start tracking
  }, [])

  useEffect(() => {
    return trackUserLocation() // cleanup
  }, [])

  return (
    <View>
    <Text>Lat: {latitude}</Text>
    <Text>Lng: {longitude}</Text>
  </View>
  )
}

const mapState = ({ latitude, longitude }) => ({ latitude, longitude })

export default App = connect(mapState)(_App)


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

1 Like

And obviously, if you need anything clarified, let me know

Thanks, these examples clear up some things for me :slight_smile: