Geolocation clearWatch(watchId) does not stop location tracking (React Native)

1.8k views Asked by At

I'm trying to create simple example of location tracker and I'm stuck with following case. My basic goal is to toggle location watch by pressing start/end button. I'm doing separation of concerns by implementing custom react hook which is then used in App component:

useWatchLocation.js

import {useEffect, useRef, useState} from "react"
import {PermissionsAndroid} from "react-native"
import Geolocation from "react-native-geolocation-service"

const watchCurrentLocation = async (successCallback, errorCallback) => {
  if (!(await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION))) {
    errorCallback("Permissions for location are not granted!")
  }
  return Geolocation.watchPosition(successCallback, errorCallback, {
    timeout: 3000,
    maximumAge: 500,
    enableHighAccuracy: true,
    distanceFilter: 0,
    useSignificantChanges: false,
  })
}

const stopWatchingLocation = (watchId) => {
  Geolocation.clearWatch(watchId)
  // Geolocation.stopObserving()
}

export default useWatchLocation = () => {
  const [location, setLocation] = useState()
  const [lastError, setLastError] = useState()
  const [locationToggle, setLocationToggle] = useState(false)
  const watchId = useRef(null)

  const startLocationWatch = () => {
    watchId.current = watchCurrentLocation(
      (position) => {
        setLocation(position)
      },
      (error) => {
        setLastError(error)
      }
    )
  }

  const cancelLocationWatch = () => {
    stopWatchingLocation(watchId.current)
    setLocation(null)
    setLastError(null)
  }

  const setLocationWatch = (flag) => {
    setLocationToggle(flag)
  }

  // execution after render when locationToggle is changed
  useEffect(() => {
    if (locationToggle) {
      startLocationWatch()
    } else cancelLocationWatch()
    return cancelLocationWatch()
  }, [locationToggle])

  // mount / unmount
  useEffect(() => {
    cancelLocationWatch()
  }, [])

  return { location, lastError, setLocationWatch }
}

App.js

import React from "react"
import {Button, Text, View} from "react-native"

import useWatchLocation from "./hooks/useWatchLocation"

export default App = () => {
  const { location, lastError, setLocationWatch } = useWatchLocation()
  return (
    <View style={{ margin: 20 }}>
      <View style={{ margin: 20, alignItems: "center" }}>
        <Text>{location && `Time: ${new Date(location.timestamp).toLocaleTimeString()}`}</Text>
        <Text>{location && `Latitude: ${location.coords.latitude}`}</Text>
        <Text>{location && `Longitude: ${location.coords.longitude}`}</Text>
        <Text>{lastError && `Error: ${lastError}`}</Text>
      </View>
      <View style={{ marginTop: 20, width: "100%", flexDirection: "row", justifyContent: "space-evenly" }}>
        <Button onPress={() => {setLocationWatch(true)}} title="START" />
        <Button onPress={() => {setLocationWatch(false)}} title="STOP" />
      </View>
    </View>
  )
}

I have searched multiple examples which are online and code above should work. But the problem is when stop button is pressed location still keeps getting updated even though I invoke Geolocation.clearWatch(watchId).

I wrapped Geolocation calls to handle location permission and other possible debug stuff. It seems like watchId value that is saved using useRef hook inside useWatchLocation is invalid. My guess is based on attempting to call Geolocation.stopObserving() right after Geolocation.clearWatch(watchId). Subscription stops but I get warning:

Called stopObserving with existing subscriptions.

So I assume that original subscription was not cleared.

What am I missing/doing wrong?

EDIT: I figured out solution. But since isMounted pattern is generally considered antipattern: Does anyone have a better solution?

1

There are 1 answers

1
zups On

Ok, problem solved with isMounted pattern. isMounted.current is set at locationToggle effect to true and inside cancelLocationWatch to false:

const isMounted = useRef(null)

...
    
useEffect(() => {
        if (locationToggle) {
          isMounted.current = true              // <--
          startLocationWatch()
        } else cancelLocationWatch()
        return () => cancelLocationWatch()
      }, [locationToggle])
    
...    

const cancelLocationWatch = () => {
        stopWatchingLocation(watchId.current)
        setLocation(null)
        setLastError(null)
        isMounted.current = false               // <--
      }

And checked at mount / unmount effect, success and error callback:

const startLocationWatch = () => {
    watchId.current = watchCurrentLocation(
      (position) => {
        if (isMounted.current) {                // <--
          setLocation(position)
        }
      },
      (error) => {
        if (isMounted.current) {                // <--
          setLastError(error)
        }
      }
    )
  }