A photograph of the author, Alex Kharouk

Alex Kharouk

Creating a custom Geolocation Hook

At work, we are building a greenfield application that gets the user's location and checks if they are nearby one of our retail stores. It is not a challenging premise since the browser offers an excellent geolocation API.1

It is a two-step process. First, we get the user's location, and then we make an API call to see if the store they are supposed to be at is nearby. At first, I thought exactly like that. Two steps, two hooks. I started building the solution in a useEffect since we are creating a side effect within our component function; well, two side effects. One to interact with the Geolocation API, and one that interacts with a backend API. So, I created the first custom hook, useGeolocation:

hooks/useGeolocation.ts
import { useEffect, useState } from 'react';
import { handleLocationError, LocationErrorCodes } from 'utils';
const useGeolocation = () => {
const [coordinates, setCoordinates] = useState({
coords: { latitude: 0, longitude: 0 },
hasAcceptedLocation: false,
});
const [errCode, setErrCode] = useState(0);
const updateCoordinates = (data) => {
setCoordinates(data);
};
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
updateCoordinates,
(err) => {
setErrCode(handleLocationError(err));
}
);
} else {
setErrCode(LocationErrorCodes.UNKNOWN);
}
}, []);
return [coordinates, errCode];
};

It is that simple. I have some utils function that handles the error codes. I use the getCurrentPosition method from the API to get the user's location by passing the updateCoordinates callback function that updates the state with the coordinates.

Omitting the sensitive information, I also wrote up a second hook, useStoreLocationCheck, which uses the useGeolocation hook to get the user's location. Then, I made an API call to check if the user was nearby the store.

hooks/useStoreLocationCheck.ts
import { useEffect, useState } from 'react';
import {
PROXY_URL,
storeDetails,
testCoords
} from 'utils/constants';
type JSONObject = {
[key: string]: string | number | boolean | JSONObject;
};
const useStoreLocationCheck = (location, isTest: boolean) => {
const [isError, setIsError] = useState(false);
const [isNearby, setIsNearby] = useState<boolean>();
let { latitude, longitude } = location.coords;
useEffect(() => {
if (isTest) {
// eslint-disable-next-line react-hooks/exhaustive-deps
latitude = testCoords.latitude;
// eslint-disable-next-line react-hooks/exhaustive-deps
longitude = testCoords.longitude;
}
const fetchStores = async () => {
setIsError(false);
try {
const response = await fetch(
`${PROXY_URL}?lat=${latitude}&lng=${longitude}`
);
const stores = await response.json();
const checkIsNearby = await stores.results.some(
(store: JSONObject) => store.id === storeDetails.id
);
setIsNearby(checkIsNearby);
} catch (error) {
setIsError(true);
// Prod-level error handling 🧑🏼‍🍳
}
};
fetchStores();
}, [longitude, latitude]);
return { isError, isNearby };
};

I decided to pass the location details (which we would get from the first custom hook). I also created some initial states for error handling and returning an isNearby variable that will let my application know if the user is nearby the store.

It worked, but it felt considerably brittle. We are overriding values passed into the hook since we might need to test a dummy location. Also, there is a bit of duplication with getting the longitude and latitude, so the next step was a combination of the two custom hooks.

hooks/useLocationProximity.ts
import { useState, useEffect } from 'react';
import {
PROXY_URL,
storeDetails,
TEST_URL
} from 'utils/constants';
type JSONObject = {
[key: string]: string | number | boolean | JSONObject;
};
const useLocationProximity = () => {
const [storeData, setStoreData] =
useState<GeolocationPosition>();
const [isNearby, setIsNearby] = useState(false);
const [locationError, setLocationError] =
useState<GeolocationPositionError>();
const updateCoords = async (data: GeolocationPosition) => {
const { latitude, longitude } = data.coords;
try {
// Uncomment to test locally:
// const response = await fetch(TEST_URL);
const response = await fetch(
`${PROXY_URL}?lat=${latitude}&long=${longitude}`
);
const stores = await response.json();
const checkIsNearby = await stores.results.some(
(store: JSONObject) => store.id === storeDetails.id
);
setStoreData(stores);
setIsNearby(checkIsNearby);
} catch (error) {
// Prod-level error handling
}
};
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
updateCoords,
(err) => {
setLocationError(err);
}
);
}
}, []);
return { storeData, isNearby, locationError };
};

💥 Boom. Combining the two hooks, useGeolocation and useStoreLocationCheck, I refactored it into a neater, cleaner solution for my use case. The key takeaway is that now I am not using the more general useGeolocation, but I still have access to it if need be.