Home > Blockchain >  react 18 - How to load a page only once depending on geoLocation value determined?
react 18 - How to load a page only once depending on geoLocation value determined?

Time:10-14

This reloading loop in react has caused large number of GeoLocation API calls so trying to fix with minimal renders.

The problem with code below is that userLocation once obtained, triggers useEffect and causes re-render. This in turn causes userLocation to be fetched again causing another render and so on.. If I dont use userLocation in the useEffect trigger, the location does not show because location is not ready yet.

    export default function userHomePage(props) {
    ..
const [location, setLocation] = useState([]);
    ..
      useEffect(() => {
        setLoadUserHomePage(true);
      }, [userLocation]);
    
    
      userLocation = getUserGeoLocation(latFromDB, longFromDB, location, setLocation); 
    ...
    ..
        return (
        loadUserHomePage?
        (
        <UserPage/>
         ):
        (<Loading/>))

Here is getUserGeoLocation function

//get user geolocation
export function getUserGeoLocation(lat, long, location, setLocation) {
  //Look for browser coordinates all the time to compare with coordinates from db
  if (!lat) { //coordinates are not passed so get from browser
    if ("geolocation" in navigator) {      
      navigator.geolocation.getCurrentPosition(
        (position) => successPosition(position.coords.latitude, position.coords.longitude),
        (err) => deniedPosition(err)            
      );
    } 
  }
  else {//coordinates passed so use it to compare with what was obtained from browser
    if ("geolocation" in navigator) {      
      navigator.geolocation.getCurrentPosition(
        (position) => successPosition(position.coords.latitude, position.coords.longitude, lat, long),
        (err) => deniedPosition(err)            
      );
    } 
    
  }
  
function deniedPosition (error) {
  setLocation (false);
};

async function successPosition (lat, long, latPassed, longPassed) { 
    //find if both lat and browser lat/long have deviated too much
    if (lat && latPassed) {
        const THRESHOLD = 0.3;//how much can new coordinates of user(from browser) deviate from old(from db)
        function isDiffBeyondThreshold(num1, num2, threshold) {
          return Math.abs(num1 - num2) > threshold;
        }  
        const islatDeviated = isDiffBeyondThreshold(lat, latPassed, THRESHOLD)
        const islongDeviated = isDiffBeyondThreshold(long, longPassed, THRESHOLD)
        if (!islatDeviated & !islongDeviated) {
          console.log("Coordinates in browser have not deviated from whats in Database")
          setLocation(["use_whats_already_in_db",,,]);
        }
        else {
          checkGeoCallsCount();
        }
    }
    else if (lat) {
          //coordinates not passed but use lat from browser so we can call api after checking count
          checkGeoCallsCount();
          return;
      }

    async function checkGeoCallsCount() {
        const MAX_API_CALLS_PER_DAY = 99;//limit API calls runaway

        // set Google Maps Geocoding API for purposes of quota management. 
        let res = await fetch('/api/analytics/geocodeUsage', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
        });
      
        if (res.status === 200) {
          res = await res.json();
          console.log("Browser Coordinates has deviated a lot from coordinates passed in db ");
          if (res.usage.count < MAX_API_CALLS_PER_DAY) {
            Geocode.setApiKey(process.env.GOOGLE_MAP_API);
            pokeGoogle();
          }
          else {
            Geocode.setApiKey("");//dont set an api key as limit reached
            //do not set location here - it will result in ifnite loop as location is also set below
            //locaion change here and below will force rerender resulting in infinite loop
            //as location keeps switching from one to another
            return;
          }
        }
      }

  function pokeGoogle() {
        console.log("Google API called")
        // set response language. Defaults to english.
        Geocode.setLanguage("en");
        //for testing just setting something but in Prod, it wil not have next 2 lines
        setLocation(["Setting TEST Location", lat, long,]);
        return; 
        // set location_type filter . Its optional.
        // google geocoder returns more that one address for given lat/lng.
        // In some case we need one address as response for which google itself provides a location_type filter.
        // So we can easily parse the result for fetching address components
        // ROOFTOP, RANGE_INTERPOLATED, GEOMETRIC_CENTER, APPROXIMATE are the accepted values.
        // And according to the below google docs in description, ROOFTOP param returns the most accurate result.
        Geocode.setLocationType("GEOMETRIC_CENTER");
        // Get formatted address, city, state, country from latitude & longitude when
        // Geocode.setLocationType("ROOFTOP") enabled
        // the below parser will work for most of the countries
        Geocode.fromLatLng(lat, long).then(
          (response) => {
            const address = response.results[0].formatted_address;
            let city, state, country;
            for (let i = 0; i < response.results[0].address_components.length; i  ) {
              for (let j = 0; j < response.results[0].address_components[i].types.length; j  ) {
                switch (response.results[0].address_components[i].types[j]) {
                  case "locality":
                    city = response.results[0].address_components[i].long_name;
                    break;
                  case "administrative_area_level_1":
                    state = response.results[0].address_components[i].long_name;
                    break;
                  case "country":
                    country = response.results[0].address_components[i].short_name;
                    break;
                }
              }

            }      
            setLocation([city   ", "   state   ", "   country, lat, long]);
          },
          (error) => {
            console.log(error)
            //do not set location here either
        }
      );
  }
}
return (location)
}

CodePudding user response:

Convert your userLocation variable to useMemo, which will be triggered only if something inside depsArray is changed.

Upd: You can modify getUserGeoLocation to remove location from arguments. Due to it is used only in return statement and due to everything inside of this function is based on async and callbacks - the returning value will be absolutely identical to the value that was passed with arguments. What will be even better - to convert this function to return actual promise with the result so you will be able to get rid of both, location and setLocation as an arguments. It will make your function much clearer and reusable.

But as a quick fix -

  1. Remove location from arguments of getUserGeoLocation.
  2. Delete the return (location) line.

Next fix to get some workaround later (based on previous fix):

function getUserGeoLocationWrapped(lat, long){
  return new Promise((resolve, reject) => {
    getUserGeoLocation(lat, long, resolve);
  })
}

function getUserGeoLocation(lat, long, setLocation) {
// ...
}

And the final code:

const latFromDB = 123;
const longFromDB = 345;
const [location, setLocation] = useState([]);
const [loadUserHomePage, setLoadUserHomePage] = useState(false);

useEffect(() => {
  getUserGeoLocationWrapped(latFromDB, longFromDB).then((res) => {
    setLocation(res);
    setLoadUserHomePage(true);
  });
}, [latFromDB, longFromDB]);
  • Related