Use Geolocation API with React Hooks

April 15, 2020

There are some interesting and useful Web APIs built into the browsers and Geolocation API is one of them. It is used to determine the users location using longitude and latitude coordinates. However there are some difficulties which you will need to count:

  1. It’s asynchronous
  2. Needs permission from the user
  3. Decide which approach will you need

How to use the API?

Once you verified that the target browser supports it then you can access the location data by calling the navigator.geolocation’s getCurrentPosition method or by assigning a location watch listener using the watchPosition. The last one will emit the results if the device location has changed.

It’s async. This means that if you call the API it will take unpredictable time for the device to return the current coordinates from the user’s location. You can handle this via callbacks:

const successHandler = position => console.log(position.coord);

const errorHandler = error => console.error(error.message);

navigator.geolocation.getCurrentPosition(successHandler, errorHandler);

// Or using the watch method:

const locationWatchId = navigator.geolocation.watchPosition(successHandler, errorHandler);

To listen the location changes you can use the watchPosition method, but don’t forget to clear the listener if you don’t need it anymore to avoid memory leaks. You can do that by calling the clearWatch method with the listener id:

navigator.geolocation.clearWatch(locationWatchId);

Options

You can provide few options to it:

  • enableHighAccuracy: The API can provide more accurate coordinates, but it costs in slower response time.
  • timeout: You can set the response timeout in milliseconds which means that it will call the error callback if the device doesn’t send any location information during that time.
  • maximumAge: You are able to set the time in milliseconds while the API can return the values from the cache

Usage:

export const geolocationOptions = {
  enableHighAccuracy: true,
  timeout: 1000 * 60 * 1, // 1 min (1000 ms * 60 sec * 1 minute = 60 000ms)
  maximumAge: 1000 * 3600 * 24 // 24 hour
};

navigator.geolocation.getCurrentPosition(successHandler, errorHandler, geolocationOptions);

How can you use it in your custom React Hooks?

*This section requires a basic understanding how React hooks works.

I have prepared an example using the basic Create React App starter to demonstrate how this native Web API works with React Hooks. You can find the full source code here: Open the Repo

Using the current location

Create a custom hook which will call the Geolocation API using the getCurrentPosition method.

First you need to verify if the geolocation is supported in the target browser. To do this you can check if the navigator has the geolocation property. Later you can display user friendly errors, but to do this you should create a local state to hold the possible error messages.

const useCurrentLocation = () => {
  // store error message in state
  const [error, setError] = useState();

  useEffect(() => {
    // If the geolocation is not defined in the used browser you can handle it as an error
    if (!navigator.geolocation) {
      setError('Geolocation is not supported.');
      return;
    }
  }, []);
};

You have to prepare your custom hook to store the Geolocation API results locally, so you should extend your state with another value called location and expose it from the hook for further usage.

const useCurrentLocation = () => {
  // ...

  // store location in state
  const [location, setLocation] = useState();

  // ...

  // Expose location result and the possible error message
  return { location, error };
};

Okay, so you have prepared everything to call the actual Geolocation API method. Right?

Not yet.

You will need to pass the success and error callbacks to the getCurrentPosition method, but you haven’t created these handlers yet. Let’s do that:

const useCurrentLocation = () => {
  // ...

  // Success handler for geolocation's `getCurrentPosition` method
  const handleSuccess = position => {
    const { latitude, longitude } = position.coords;

    setLocation({
      latitude,
      longitude
    });
  };

  // Error handler for geolocation's `getCurrentPosition` method
  const handleError = error => {
    setError(error.message);
  };

  // ...
};

The handleSuccess will destruct the longitude and latitude values from the location results and set the new values into your local state variable.

The handleError will set any error message coming from the Geolocation API into the local error state variable. This callback is called when getting the location times out or the user denies the asked permissions.

Now you are ready to call what you need to retrieve the user’s location. To do this you can call the above mentioned getCurrentPosition method inside your useEffect hook.

useEffect(() => {
  // ...

  // Call the Geolocation API
  navigator.geolocation.getCurrentPosition(handleSuccess, handleError);

  // ...
}, []);

You can add some additional behavior to your newly created custom hook with Geolocation options. I decided to pass these settings as an optional object parameter to the custom hook.

Note: You use geolocation inside useEffect and if you would like to pass your options argument to the getCurrentPosition method then you have to add it to the useEffect hook’s dependency list. This will help you to rerun the location API again with the new options if the reference of this parameter changes.

const useCurrentLocation = (options = {}) => {
  // ...

  useEffect(() => {
    // ...

    // Call the Geolocation API with options
    navigator.geolocation.getCurrentPosition(handleSuccess, handleError, options);

    // ...
    // Add options parameter to the dependency list
  }, [options]);

  // ...
};

Usage of the useCurrentLocation hook

const geolocationOptions = {
  // Using this option you can define when should the location request timeout and
  // call the error callback with timeout message.
  timeout: 1000 * 60 * 1 // 1 min (1000 ms * 60 sec * 1 minute = 60 000ms)
};

function App() {
  const { location, error } = useCurrentLocation(geolocationOptions);

  return (
    <div>
      <h1>HTML Geolocation API with React Hooks example</h1>;
      {location ? (
        <code>
          Latitude: {location.latitude}, Longitude: {location.longitude}
        </code>
      ) : (
        <p>Loading...</p>
      )}
      {error && <p>Location Error: {error}</p>}
    </div>
  );
}

See the result in the browser: Display Geolocation API results in browser

Listen to device location changes

What if you need to track the user’s location while they are using your application?

This would be the right time to choose the watchPosition over the getCurrentLocation. However it can be useful any time when you need to interrupt the location request manually.

For example: if your application makes possible for the users to set the position manually while you already started to access their location automatically.

This is not possible when you are using the current location request, but you can save yourself with the provided location listener. You can prepare for these cases combined the watchPosition and the clearWatch methods.

To achieve this functionality you just need to do some minor modifications on the previously created useCurrentLocation custom hook.

First what you need is create a ref variable in the custom hook. This will help you to keep the reference of the listener instance by assigning the returned value of the watchPosition to it.

const useWatchLocation = (options = {}) => {
  // ...

  // save the returned id from the geolocation's `watchPosition`
  // to be able to cancel the watch instance.
  const locationWatchId = useRef(null);

  // ...
};

Assign the location listener inside useEffect similar like you did for the getCurrentLocation.

const useWatchLocation = (options = {}) => {
  // ...

  useEffect(() => {
    // ...

    // Start to watch the location with the Geolocation API
    locationWatchId.current = navigator.geolocation.watchPosition(handleSuccess, handleError, options);

    // ...
  }, [options]);

  // ...
};

Last thing you have to do is implement the clear up logic. React can unmount components which are using this location hook. To prepare for this case you have to pay attention to clear every listener to avoid any memory leaks in your application. You can reach this by extending the useEffect with its returned function. This can be defined by you, so in this case the returned function will call the clearWatch method from the Geolocation API using the tracked listener id.

Also if you need to cancel the location tracking programmatically, then you can expose the implemented clear up function from your custom hook.

const useWatchLocation = (options = {}) => {
  // ...

  // Clears the watch instance based on the saved watch id
  const cancelLocationWatch = () => {
    if (locationWatchId.current && navigator.geolocation) {
      navigator.geolocation.clearWatch(locationWatchId.current);
    }
  };

  useEffect(() => {
    // ...

    // Clear the location watch instance when React unmounts the used component
    return cancelLocationWatch;
  }, [options]);

  // ...

  // Exposed results and public cancel method to clear the location listener manually.
  return { location, cancelLocationWatch, error };
};

Usage of the useWatchLocation hook

function App() {
  const { location, cancelLocationWatch, error } = useWatchLocation();

  useEffect(() => {
    if (!location) return;

    // Cancel location watch after 3sec once the location was found.
    setTimeout(() => {
      cancelLocationWatch();
    }, 3000);
  }, [location, cancelLocationWatch]);

  // ...
}

Source code: Open the Repo. Feel free to check out and play around with it. You can extend the code with your custom logic, optimize it to avoid unnecessary rerenders in your components etc.

You can read more about the Geolocation API in the docs.

Follow me on Twitter Norbert Bartos