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 -
- Remove
location
from arguments ofgetUserGeoLocation
. - 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]);