DEV Community

Cover image for Custom React hooks to use the browser’s APIs
Sebastien Castiel
Sebastien Castiel

Posted on • Updated on

Custom React hooks to use the browser’s APIs

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:

// returns null if no value exists for the given key
const value = localStorage.getItem('key')
localStorage.setItem('key', 'value')
Enter fullscreen mode Exit fullscreen mode

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:

const usePersistedState = (key, initialValue) => {
  const [value, setValue] = useState(initialValue)
  return [value, setValue]
}
Enter fullscreen mode Exit fullscreen mode

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:

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

return [value, setAndPersistValue]
Enter fullscreen mode Exit fullscreen mode

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.

const [value, setValue] = useState(initialValue)

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

Here is how you can do the other way:

const [value, setValue] = useState(null)

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

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:

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

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:

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)
Enter fullscreen mode Exit fullscreen mode

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:

const inputRef = useRef()

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

return <input ref={inputRef} defaultValue="Hello" />
Enter fullscreen mode Exit fullscreen mode

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:

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

const useElementSize = (elementRef) => {
  const [width, setWidth] = useState(undefined)
  const [height, setHeight] = useState(undefined)
  // ...
  return [width, height]
}
Enter fullscreen mode Exit fullscreen mode

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

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])
Enter fullscreen mode Exit fullscreen mode

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.

navigator.geolocation.getCurrentPosition(
  (res) => console.log(res.coords.latitude, res.coords.longitude),
  (err) => console.log('Impossible to get current position')
)
Enter fullscreen mode Exit fullscreen mode

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').

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 }
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  }
}
Enter fullscreen mode Exit fullscreen mode

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. 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), where I regularly post about React, hooks, frontend in general, and other subjects 😉

Oldest comments (0)