Search code examples
javascriptreactjsopenlayers

React, setstate does not activate inside useEffect function for one variable, works for another


I am trying to display a map with the users current location combined with some additional geometry. I can access these data and display them, but when it is time to show another page I want to unsubscribe from the location updates. To do this I need an id (according to the documentation of the navigator.geolocation). I am trying to store this id in a state variable, but for some reason I cannot set this id inside my useEffect function.

The puzzling thing is that I also store the geometry data in a state variable, and I can set that just fine from inside the useEffect loop. I just cannot figure out what is the difference between the features and the watcherId state variables. When I run this code the waId gets set, but the watcher id always remains -1, and therefore I cannot use it to unsubscribe from the location changed event.

import React, { useState, useEffect } from 'react';
import GeoJSON from 'ol/format/GeoJSON';
import Feature from 'ol/Feature';
import {Point} from 'ol/geom';
import {transform, fromLonLat} from 'ol/proj';
import MapWrapper from './Base_MapWrapper';

function Card_Map(props) {
    const [ features, setFeatures ] = useState([]);
    const [ watcherId, setWatcherId ] = useState(-1);

    useEffect( () => {
        const wktOptions = {
            dataProjection: 'EPSG:4326',
            featureProjection: 'EPSG:3857'
        }
        const parsedFeatures = new GeoJSON().readFeatures(props.jsonData, wktOptions);

        let waId = -1;
        if (navigator.geolocation) {
            console.log('location available');
            waId = navigator.geolocation.watchPosition(function(position) {
                let coorArr = [position.coords.longitude, position.coords.latitude];
                let newCoors = transform(coorArr, 'EPSG:4326', 'EPSG:3857');
                let fPoint = new Feature({
                    geometry: new Point(newCoors)
                });
                let theFeatures = [fPoint, ...parsedFeatures];
                setFeatures(theFeatures);
            });
            console.log(waId);
            setWatcherId(waId);
        }
        else {
            setFeatures(parsedFeatures);
        }
        console.log(watcherId);

        //this is the destructor 
        return () => {
            if (watcherId != -1)
            {
                navigator.geolocation.clearWatch(watcherId);
            }
        }
    },[]);

    return (
        <div>
            <MapWrapper features={features} cssClass={props.cssClass} />
        </div>
    ) 
}

export default Card_Map

Solution

  • Your problem is the fact that you are trying to use watcherId which will keep a reference of the value before calling setWatcherId.

    I'm pretty sure that you have a warning saying that you should use watcherId as a dependency, but this will trigger another re-render and a new set and so on.

    You don't actually need a state here, you can just use waId so you should end up with the following:

    function Card_Map(props) {
        const [ features, setFeatures ] = useState([]);
    
        useEffect( () => {
            const wktOptions = {
                dataProjection: 'EPSG:4326',
                featureProjection: 'EPSG:3857'
            }
            const parsedFeatures = new GeoJSON().readFeatures(props.jsonData, wktOptions);
    
            let waId = -1;
            if (navigator.geolocation) {
                console.log('location available');
                waId = navigator.geolocation.watchPosition(function(position) {
                    let coorArr = [position.coords.longitude, position.coords.latitude];
                    let newCoors = transform(coorArr, 'EPSG:4326', 'EPSG:3857');
                    let fPoint = new Feature({
                        geometry: new Point(newCoors)
                    });
                    let theFeatures = [fPoint, ...parsedFeatures];
                    setFeatures(theFeatures);
                });
            }
            else {
                setFeatures(parsedFeatures);
            }
    
            //this is the destructor 
            return () => {
                if (waId != -1)
                {
                    navigator.geolocation.clearWatch(waId);
                }
            }
        },[]);
    
        return (
            <div>
                <MapWrapper features={features} cssClass={props.cssClass} />
            </div>
        ) 
    }
    

    If you do need the id in another place, keep the state, but still use waId in the cleanup function.