Custom React hooks to use the browser’s APIs
Tags: react, javascript
Published: 2021-01-18 14:23:11

A thing you often need to do in React is access the browser’s APIs. These APIs represent side effects, and most of the time, you will want to store what they return in a local state: a perfect opportunity to write some custom hooks that you’ll be able to reuse across your applications.

## Access the local storage

The browser’s local storage is a place you can save some values, so they are persisted for when you leave the page and go back. It’s key-value storage and its API is quite straightforward:

```jsx
// returns null if no value exists for the given key
const value = localStorage.getItem('key')
localStorage.setItem('key', 'value')
```

If you are not familiar with it, you can play with it just by using the console in your browser. Try to create some values, refresh the page, and get them back. Note that you can only store string values.

Here, we’ll write an improved version of the `useState` hook that persists the value in the local storage. If the user refreshes the page, the state will be initialized with the stored value.

We want our hook to be used almost the same way as `useState`, so we will make it return the same kind of array, with the current value and a setter. It will accept as parameters the initial state value and the key used to store the value in the local storage.

Let’s start by just using a classic state provided by `useState`:

```jsx
const usePersistedState = (key, initialValue) => {
  const [value, setValue] = useState(initialValue)
  return [value, setValue]
}
```

First thing, when setting a new value, we want to store this new value in the local storage using `localStorage.setItem`. Let’s create a function doing this operation just after calling the original `setValue`, and return this function in place of `setValue`:

```jsx
const setAndPersistValue = (newValue) => {
  setValue(newValue)
  localStorage.setItem(key, newValue)
}

return [value, setAndPersistValue]
```

Then, when the component is mounted, we want to get the currently stored value from the state, and if it exists, update our state’s value with it.

At that point, we have a choice to make: what value do we want to return before we get the value from the local storage? Two solutions:

1. We return the provided `initialValue` and replace it with the existing value if it exists;
2. We return `null` or `undefined`, then the current value if it exists, the provided `initialValue` otherwise.

There is no absolute best choice here; it depends on your need. But if you intend to distribute this hook to other people, your documentation should mention the choice you made.

Here I chose the first way to do it and kept using the `initialValue`.

```jsx
const [value, setValue] = useState(initialValue)

useEffect(() => {
  const existingValue = localStorage.getItem(key)
  if (existingValue !== null) {
    setValue(existingValue)
  }
}, [key])
```

Here is how you can do the other way:

```jsx
const [value, setValue] = useState(null)

useEffect(() => {
  const existingValue = localStorage.getItem(key)
  if (existingValue !== null) {
    setValue(existingValue)
  } else {
    setValue(initialValue)
  }
}, [key])
```

Our hook is complete, let’s see how to use it. We’ll create a component with an input, and use our hook to persist the value entered in the input in the local storage:

```jsx
const Comp = () => {
  const [name, setName] = usePersistedState('name', 'John Doe')
  return (
    <input
      type="text"
      value={name}
      onChange={(event) => setName(event.target.value)}
    />
  )
}
```

Don’t you find it pleasant to use our custom hook almost the same way we would use `useState`? And that we hid in our hook most of the complexity to access the local storage so the developers using it won’t even be aware of it?

## Get an element’s size

Another thing you might want to do is adapt your component's behavior depending on some element size. What would be cool is having a hook returning me the current width and height of any element I want in realtime. Let’s see how we can create such a hook.

First, let’s put React aside for a minute and see how to get the size of a DOM element using plain JavaScript. Modern browsers offer an object `ResizeObserver` that we can use for that. Its API is not the easiest to apprehend at first sight; for our use case, it consists in:

1. Creating an instance of `ResizeObserver`, passing it a callback executed each time one of the observed elements’ size has changed;
2. Subscribe to observe each element we want to.

Here is an example displaying in the console the width and height of an element each time it is modified:

```jsx
const element = document.querySelector('#myElement')

// 1.
const resizeObserver = new ResizeObserver((entries) => {
  for (let entry of entries) {
    if (entry.contentRect) {
      console.log(entry.contentRect.width, entry.contentRect.height)
    }
  }
})

// 2.
resizeObserver.observe(element)
```

Note that we loop through several `entries` in the callback given to `RedizeObserver`; this is because an observer can observe several elements, although we will only observe one here.

Let’s come back to React: to know the size of a DOM element, we need first to get this element. We will need to use a _ref_, via the `useRef` hook. We saw how refs were useful in a previous lesson when dealing with async code; here is another common use case.

By creating a ref with `useRef` and passing it as the `ref` prop of any HTML element rendered in your component, you can access the DOM element itself via `yourRef.current`:

```jsx
const inputRef = useRef()

useEffect(() => {
  console.log(inputRef.current.value)
  // logs “Hello!”
}, [inputRef])

return <input ref={inputRef} defaultValue="Hello" />
```

Here we need this ref to _observe_ it via our `ResizeObserver`, so we will pass it as a parameter to our custom hook. Here is how we expect to use our hook; let’s name it `useElementSize`:

<!-- prettier-ignore -->
```jsx
const Comp = () => {
  const divRef = useRef()
  const [width, height] = useElementSize(divRef)

  return (
    <div
      style={{
        // Initial size
        width: 150, height: 100,
        // Makes the element resizeable
        resize: 'both', overflow: 'auto',
        // So it’s easier to resize
        border: '1px solid #191a21',
      }}
      ref={divRef}
    >
      {width}x{height}
    </div>
  )
}
```

As you can see, we want our hook to return the width and height of the element pointed by the ref, and of course, we want these values to be updated when the user resized the element.

So our hook `useElementSize` has to keep the current element’s width and height in a local state, and returns them:

```jsx
const useElementSize = (elementRef) => {
  const [width, setWidth] = useState(undefined)
  const [height, setHeight] = useState(undefined)
  // ...
  return [width, height]
}
```

The last missing piece is to create the `ResizeObserver` to update these local state values when the element is resized:

<!-- prettier-ignore -->
```jsx
useEffect(() => {
  const resizeObserver = new ResizeObserver((entries) => {
    for (let entry of entries) {
      if (entry.contentRect) {
        setWidth(entry.contentRect.width)
        setHeight(entry.contentRect.height)
      }
    }
  })
  resizeObserver.observe(elementRef.current)

  // Let’s disconnect the observer on unmount:
  return () => { resizeObserver.disconnect() }
}, [elementRef])
```

Notice that we subscribe to the observer in a `useEffect` and that we _disconnect_ the observer when the component is unmounted.

## Get the user’s geolocation

To conclude this lesson, let’s see another example of the browser’s API, which you can access very elegantly via a custom hook: the _geolocation_ API. As its name suggests, the idea is to get the user’s location, meaning the latitude and longitude of their position. Of course, this API can be used only on devices supporting it (mobile devices, modern browsers) and only if the user agreed to be geolocated.

You can access this API using the `navigator.geolocation` object, more precisely its method `getCurrentPosition`. It accepts two callback parameters: one executed when the browser successfully returns the current position, the other when an error occurred, meaning the device does not support geolocation or the user didn’t authorize the page to get it.

```jsx
navigator.geolocation.getCurrentPosition(
  (res) => console.log(res.coords.latitude, res.coords.longitude),
  (err) => console.log('Impossible to get current position')
)
```

To return the user’s current position via a custom hook, we will apply the same pattern we used in the previous two examples:

- Keep a local state with the position.
- Call the geolocation API in a `useEffect` to update the state.
- Return its values.

We’ll introduce a small difference, though: since we want to handle the error case, we will also return a `status` attribute indicating if we are waiting for the position (`'pending'`), if we fetched it successfully (`'success'`), or if an error occurred (`'error'`).

```jsx
const useGeolocation = () => {
  const [status, setStatus] = useState('pending')
  const [latitude, setLatitude] = useState(undefined)
  const [longitude, setLongitude] = useState(undefined)

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      (res) => {
        setStatus('success')
        setLatitude(res.coords.latitude)
        setLongitude(res.coords.longitude)
      },
      (err) => {
        console.log(err)
        setStatus('error')
      }
    )
  }, [])

  return { status, latitude, longitude }
}
```

In the components using this hook, we can then use the returned `status` attribute to decide what to display:

```jsx
export const Comp = () => {
  const { status, latitude, longitude } = useGeolocation()

  switch (status) {
    case 'pending':
      return <p>Waiting for geolocation…</p>
    case 'success':
      return (
        <p>Your location: ({latitude}, {longitude})</p>
      )
    case 'error':
      return <p>Have you authorized me to access your geolocation?</p>
  }
}
```

With these three custom hooks examples to use the browser’s APIs, you probably notice that the recipe is very similar. With this in mind, you are now able to write many custom hooks. It doesn’t mean that they will solve every problem, but they are an additional tool you can use to make your code cleaner, especially when you want to access features provided by the browser.

**What are your favorite custom hooks?**

---

_If you liked this post, I talk a lot more about React and hooks in my new course [useEffect.dev](https://useeffect.dev/?ref=dev.to). Its goal is to help you understand how they work, how to debug them, and how to solve common problems they can cause._

_You can also [follow me on Twitter (@scastiel)](https://twitter.com/scastiel), where I regularly post about React, hooks, frontend in general, and other subjects 😉_