Why my subscribe method doesn't use new state but only initial state?

Viewed 35

I want to calculate speed in my gps app, to do that I use states to save my current ans previous time, latitude and longitude. I initialize the subscribe function in useEffect, however the previous state of time, lat and lon is always the same even though it should be updated several times. How can I make this work without using localStorage to hold these values?

 const [lat, setLat] = useState(51.55);
    const [lon, setLon] = useState(0);
    const [prevLat, setPrevLat] = useState(0);
    const [prevLon, setPrevLon] = useState(0);
    const [time, setTime] = useState(new Date());
    const [prevTime, setPrevTime] = useState(new Date());

    useEffect(() => {
        Geolocation.getCurrentPosition().then((resp) => {

            setTime(resp.timestamp);
            setLat(resp.coords.latitude)
            setLon(resp.coords.longitude)

        }).catch((error) => {
            console.log('Error getting location', error);
        })

        const onSuccess = () => {
            console.log('success')
        }
        const onError = () => {
            console.log('error')
        }

        let watch = Geolocation.watchPosition(onSuccess, onError, { enableHighAccuracy: true });

        watch.subscribe((data) => {
            if (data && data.coords && data.coords.latitude && data.coords.longitude) {
                setPrevLat(lat);
                setPrevLon(lon);
                setPrevTime(time);
                setLat(data.coords.latitude);
                setLon(data.coords.longitude);
                setTime(data.timestamp);

                //calculate speed
                console.log(calculateSpeed(prevTime, prevLat, prevLon, data.timestamp, data.coords.latitude, data.coords.longitude));
            } else {
                alert(data.message, ' ', data.code)
            }

        });
    }, [])

In other words 4th, 5th and 6th arguments of calculateSpeed are proper values, but prevTime, prevLon and prevLat are still accordingly: date when the app started to run, 0, 0

1 Answers

This is a classic functional programming issue that you have to watch out for with closures.

What's happening is the following:

  1. Your useEffect is getting called only on the first render, since you have [] as the dependencies.
  2. It initializes watch.subscribe with an anonymous function and creates a closure! This closure has access to parent scope variables, but it takes a snapshot of those variables at the time that the closure is created!
  3. When your subscribe callback is triggered, it still operates using the same variables it had when the closure was created. This is a "stale closure".

So, how can you fix this?

One strategy is to unsubscribe your subscribe callback and re-subscribe with a fresh closure whenever the state changes by passing the state variables to the useEffect dependencies (but this will depend on the Geolocation API). See the link above for other options.

You can also easily address the issue by using a class component with a class method to replace the function in the watch.subscribe call. Class methods don't have the same stale closure problems that pure functional React hooks do.

I can't give a concrete solution to your problem because the design changes may change the desired behaviour, but I hope this gives you an understanding of the problem. Good luck!

Related