React Hook: useElementDimensions
CSS is great, but sometimes it is not enough. Sometimes you need to know the size of CSS elements in the JavaScript code, or to know where it is on the screen. In that case the useElementDimensions is perfect hook to use. This article describes what the hook is, how to implement it and shows some use cases of it.
Actually, we will look at two hooks in this article, both useStaticElementDimensions and useElementDimensions. We will start with the static hook, which has limited use cases but serves as a good example of why we need the more advanced hook.
In This Article
- useStaticElementDimensions Hook
- useElementDimensions Hook
- Cautions and Improvements
- Use Cases
- Example Usage
- Summary
useStaticElementDimensions Hook
Below you can see a JavaScript implementation of a useStaticElementDimensions hook. The hook is also available to test at CodeSandbox. The example on CodeSandbox is written in TypeScript, so if you're using it in a TypeScript project you should head over there, or get it from GitHub.
import { useCallback, useState } from 'react'
const useStaticElementDimensions = () => {
const [dimensions, setDimensions] = useState(null)
const ref = useCallback((node) => {
if (!!node) {
const domRect = node.getBoundingClientRect()
setDimensions(domRectToDimensions(domRect))
}
}, [])
return { dimensions, ref }
}
export default useStaticElementDimensions
The hooks is simple to use, but not straight forward to understand. So we will go through it in detail.
The hooks takes no arguments, but it returns an object with two properties. One of the return values is a reference ref, which we will pass to the DOM node we want to measure the size and position of. When doing that, the dimensions object returned from the hook will contain the bounding client rect which has the following attributes in its type definition:
height: number;
width: number;
x: number;
y: number;
bottom: number;
left: number;
right: number;
top: number;
This means that we with this hook will be able to detect where on the screen the node is, and also its width and height. So usage of the hook would look something like this.
const SomeComponent = () => {
const { dimensions, ref } = useStaticElementDimensions()
const { height, width, x, y } = dimensions ?? {}
return <SomeNode ref={ref}>Example</SomeNode>
}
At this point, you may wonder, why did the hook return a useCallback function as ref when it could have used a useRef reference instead? The answer is about update frequency.
This useStaticElementDimensions hook is based on callback refs, more specifically at the old React docs of how to measure DOM nodes. As you can read there, the regular ref retrieved from useRef function doesn’t notify about changes to the ref value, meaning we will only be informed about what value the node has when the component containing the referenced node mounts.
If the referenced node would be added later, using either conditional rendering or if a component is lazy loaded, the regular useRef reference wouldn't let us know that the node value has changed.
By using a callbackRef function rather than a normal ref, the reference will instead be bound to the actual node's mount and umount events. So, with the useCallback Ref, the hook does its job, but there's a bunch of use cases which isn't handled, which is probably why the example isn't included in the React docs anymore.
Don't worry Napoleon, we have another hook
useElementDimensions Hook
So, now we have seen a hook which we could use to check the measurements of a DOM node with JavaScript. The hook does work, but it has some flaws.
First of all, it only measures the dimensions of the node when the node mounts, meaning it will not update when the element or viewport is updated. It's just a snapshot of the element, which can be used for static components which aren't dynamically rendered, thereby the name useStaticElementDimensions.
Websites are rarely static though. Sizes of DOM elements are often preserved, but positions are definitely not static. When the user scrolls on the page, or resizes the browser window, the position of the elements changes, and the static hook above doesn't consider that.
This is why we need a more sophisticated hook for this job, the useElementDimensions hook. The useElementDimensions hook works with browser scroll and resize events, and it also handles a special case where we manually need to refresh the dimensions.
You can see the updated hook below. Once again, a TypeScript version of the hook is available on GitHub and CodeSandbox.
import useEventListener from 'hooks/useEventListener'
import { useCallback, useRef, useState } from 'react'
const useElementDimensions = () => {
const ref = useRef(null)
const [dimensions, setDimensions] = useState(null)
const refresh = useCallback(() => {
const domRect = ref.current?.getBoundingClientRect()
if (domRect) {
setDimensions(domRect)
}
}, [])
useEventListener('resize', refresh);
useEventListener('scroll', refresh, true);
return { dimensions, ref, refresh }
}
export default useElementDimensions
The imported useEventListener looks like this.
import { useEffect } from 'react'
const useEventListener = (event, listener, useCapture) => {
useEffect(() => {
if (listener) {
listener()
window.addEventListener(event, listener, useCapture)
return () => window.removeEventListener(event, listener, useCapture)
}
return () => {}
}, [event, listener, useCapture])
}
As you can see, this hook is more advanced. The helper hook, useEventListener, should be easy to understand. It's a wrapper around a useEffect which subscribes and unsubscribes to events. In this case, the useElementDimensionsHook listens on the 'resize' and 'scroll' events specifically.
useEventListener('resize', refresh);
useEventListener('scroll', refresh, true);
You may also notice, that in this hook, we have ditched the callback ref and are now using a regular useRef reference. The useEventListener does invoke the listener when the effect runs, which means that the refresh function in the useElementDimensions hook will update the dimensions according to the current value of the node ref. That will re-trigger every time the component mounts, so there is no need for a callback ref.
The dimensions, are still stored in a useState though. This is because useRef values doesn't trigger components to re-render. So if we wouldn't use useState, the components using this hook wouldn't be notified about the change.
The useState, together with the resize and scroll listener hook useEventListener and the refresh function it triggers, is enough to keep the dimension return value up to date in most use cases. The user can resize the browser and scroll however it wants, and the hook will always provide the latest size and position of the node.
However, as I mentioned before, there are some special cases to handle, and that's why the hook exposes the refresh function as a return value. For example, if the dimensions returned from the function would be used to move or resize the node it references, the dimensions would become outdated. And since neither a scroll event or browser resize has occurred, the dimensions won't be updated.
If you find cases like that you can use the refresh function to manually update the dimensions. Although, more likely, you are doing some shady things and should watch out for infinite loops and nasty bugs. So in reality, you may be more safe by keeping the refresh function internal to the hook.
Cautions and Improvements
As just mentioned, you should be careful to use the refresh function manually outside the hook. You may end up in too many rerenders or strange behaviors. Only use the exposed refresh function if you know why you are doing so.
Furthermore, you should now about the implications of using the useElementDimensions hook. Listening on scroll positions is a slow operation. Generally, it is better to use the Intersection Observer API to check if nodes are visible on screen and for similar use cases, whenever it is possible.
Additionally, you may want to add a throttle or debounce to the useElementDimensions hook. Or, if you have a quite limited use case for that hook, you may instead use the callback ref based hook useStaticElementDimensions. But remember, building responsive applications has been cool for a long time now.
I can tell you, John did update all his sites to be responsive that night
Use Cases
In most cases, you would probably want to use the useElementDimensions hook. If you can be sure you won't need it, you have an option to use the less advanced useStaticElementDimensions hook.
Either way, here are a few use cases of when a hook like this may be needed.
- To ensure a menu or dropdown doesn't end up outside the viewport
- To implement infinite scroll solutions
- For dynamic components
1. To Ensure a Menu or Dropdown Doesn't End Up Outside the Viewport
Menus such as dropdown menus and context menus are often rendered at the place the trigger button is placed. On many sites, the main menu is a hamburger menu in the top right corner of the site. Since these menus are fairly large, and often absolutely positioned, they can easily end up going out of the screen.
The same problem often occurs with dropdown menus or context menus on other places on the screen. For instance, when a dropdown menu is scrolled to be positioned at the bottom of the screen when it is being opened. In that case, the dropdown may drop out of screen and may need to open upwards instead, or at least being lift up so all of it is visible.
In these cases, a hook like useElementDimensions may be useful. A node's size and position can be compared to the window's innerWidth and innerHeight to determine if an element is inside or outside the viewport.
2. To Implement Infinite Scroll Solutions
Some more complex usage of this hook can be when a list is rendered. You are potentially implementing a infinite scroll component, which needs to recycle the list items to avoid loading too many DOM nodes into the DOM tree.
Such solutions can often involve calculating how many items which will fit within the list. This is an easy task if all elements are of the same height. That best case scenario is not always possible. Sometimes the height of elements will vary, and you may need to detect the size of the elements. That's another scenario when this hook could become useful.
3. For Dynamic Components
Components are not always static. You may render components dynamically, or even lazy load them. Or they may exist in you code but being opened with code. The useElementDimensions hook can in that case help you measuring the height and width of those components.
Example Usage
When this hook is used, it will generally look something like this.
import useElementDimensions from "hooks/useElementDimensions";
const DynamicComponent = () => {
const { dimensions, ref } = useElementDimensions();
const { height, width, x, y } = dimensions ?? {};
return (
<>
<div ref={ref}>Some element</div>
<p>Height: {height}</p>
<p>Width: {width}</p>
<p>X: {x}</p>
<p>Y: {y}</p>
</>
);
};
You can pass the ref to any DOM node. This also works for TypeScript, since the hooks uses generic types in the examples on GitHub and CodeSandbox.
Summary
useElementDimensions is a hook you can use for at least three use cases.
- To ensure a menu or dropdown doesn't end up outside the viewport
- To implement infinite scroll solutions
- For dynamic components
The hook updates the ref upon scroll and window resize events. However, listening to such events is an expensive operation. You may want to use a throttle or a debounce, or maybe use the static variant of the hook, useStaticElementDimensions, which is based on callback refs, but isn't updated when the use scrolls or the browser window change size.