Creating React usePosition() hook for getting browser's geolocation

30 June, 2019
usePosition

TL;DR

In this article we’ll create a React usePosition() hook to fetch and follow browser’s location. Under the hood we’ll use getCurrentPosition and watchPosition functions that are provided by global object navigator.geolocation. The final version of the usePosition() hook is published on GitHub and NPM and is ready to be consumed by your apps.

Why we might need usePosition() hook at all

One of the advantages of React hooks is ability to separate concerns. Instead of having a state object with, let’s say, geo-position and socket connection we might avoid using state at all and just use two different hooks that will handle the state management for us. Even more, instead of initiating browser position watcher and opening a socket connection in the same componentDidMount() callback we might split this logic into two independent hooks. This gives us cleaner and more maintainable code.

How we’re going to usePosition() hook

Let’s do some reverse engineering and imagine that we already have a usePosition() hook implemented. Here is how we might want to use it:

⚠️ All code samples below use wrong symbol =≻ instead of => for lambda functions annotations. Be aware of this while copying the code and trying to execute it since it might not work. Please replace ≻ with > manually in your code. For some reason I had issues with pasting the > symbol inside the code block. Sorry for inconvenience.

import React from 'react';
import {usePosition} from './usePosition';

export const UsePositionDemo = () => {
  const {latitude, longitude, error} = usePosition();
  return (
    <code>
      latitude: {latitude}<br/>
      longitude: {longitude}<br/>
      error: {error}
    </code>
  );
};

You see, it is only one line with usePosition() hook, and you already have the data (latitude and longitude). We don’t even use useState() and useEffect() here. Position subscription and watcher cleanup is incapsulated in usePosition() hook. Now, the redraw component magic will be handled for us by React, and we will see the block constantly being updated with the latest position value of the browser. Looks pretty neat and clean.

usePosition() hook implementation

Our custom usePosition() hook is just a JavaScript function that uses other hooks like useState() and useEffect(). It will look something like:

// imports go here...
export const usePosition = () => {
  // code goes here...
}

We will use useEffect() hook to hook to the moment in time when a component (that will consume our hook) is rendered and to subscribe to geolocation changes. We will also use useState() hook to store latitude, longitude and error message (in case if user won’t allow browser to share its position). So we need to import these hooks first:

import {useState, useEffect} from 'react';

export const usePosition = () => {
  // code goes here...
}

Let’s init a storage for position and for error:

import {useState, useEffect} from 'react';

export const usePosition = () => {
  const [position, setPosition] = useState({});
  const [error, setError] = useState(null);

  // other code goes here...
}

Let’s return a desirable values from the function. We don’t have them yet but let’s return initial values so far and fill them later:

import {useState, useEffect} from 'react';

export const usePosition = () => {
  const [position, setPosition] = useState({});
  const [error, setError] = useState(null);

  // other code goes here...
  return {...position, error};
}

Here is a key part of our hook — fetching the browser’s position. We will execute fetching logic after component has been rendered (useEffect hook).

import {useState, useEffect} from 'react';

export const usePosition = () => {
  const [position, setPosition] = useState({});
  const [error, setError] = useState(null);

  // callbacks will go here...
  useEffect(() => {
    const geo = navigator.geolocation;
    if (!geo) {
      setError('Geolocation is not supported');
      return;
    }
    watcher = geo.watchPosition(onChange, onError);
    return () => geo.clearWatch(watcher);
  }, []);
  return {...position, error};
}

In useEffect() hook we first do some checks to see if the browser is supporting navigator.geolocation. If geolocation is not supported we’re setting up an error and returning from the effect. In case if navigator.geolocation is supported we subscribe to position changes by providing an onChange() and onError() callbacks (we’ll add them in a moment). Notice that we’re returning a lambda function from useEffect(). In that lambda function we are clearing the watcher once the component is unmounted. So this subscribe/unsubscribe logic will be handled internally by our usePosition() hook and consumers shouldn’t worry about it.

Let’s now add missing callbacks:

import {useState, useEffect} from 'react';

export const usePosition = () => {
  const [position, setPosition] = useState({});
  const [error, setError] = useState(null);

  const onChange = ({coords}) => {
    setPosition({
      latitude: coords.latitude,
      longitude: coords.longitude,
    });
  };
  const onError = (error) => {
    setError(error.message);
  };
  useEffect(() => {
    const geo = navigator.geolocation;
    if (!geo) {
      setError('Geolocation is not supported');
      return;
    }
    watcher = geo.watchPosition(onChange, onError);
    return () => geo.clearWatch(watcher);
  }, []);
  return {...position, error};
}

And we’re done. The hook usePosition() may be consumed and it incapsulates only geolocation related logic.

Afterword

You may find a demo and more detailed implementation of usePosition() hook on GitHub. I hope this example was informative for you. Happy coding!

Subscribe to the Newsletter

Get my latest posts and project updates by email