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')
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]
}
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]
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:
- We return the provided
initialValue
and replace it with the existing value if it exists; - We return
null
orundefined
, then the current value if it exists, the providedinitialValue
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])
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])
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)}
/>
)
}
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:
- Creating an instance of
ResizeObserver
, passing it a callback executed each time one of the observed elements’ size has changed; - 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)
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" />
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>
)
}
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]
}
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])
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')
)
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 }
}
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>
}
}
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 😉
Top comments (1)
Why create one when you can get all awesome hooks in a single library?
Try scriptkavi/hooks. Copy paste style and easy to integrate with its own CLI