Save the element passed to a callback ref as a state
Written byPhuoc Nguyen
Created
21 Oct, 2023
Tags
Container queries, React callback refs, ResizeObserver
In our last post, we learned how to use a callback ref to create a simple container query. Let's refresh our memory with the code we wrote earlier:
tsx
const resizeObserver = new ResizeObserver(resizeCallback);
const trackSize = (ele) => {
if (ele) {
resizeObserver.observe(ele);
}
};
// Render
return (
<div ref={(ele) => trackSize(ele)}>
...
</div>
);
In this code snippet, we've passed a callback function to an element using the
`ref`
attribute. The callback function uses the DOM node representing that element to perform additional actions.However, we can't use the node in other parts of the code except within the callback function. That's where the component state comes in handy. We can store the node as a component state, making it accessible throughout the component.
Check out this sample code that demonstrates this concept:
tsx
const [node, setNode] = React.useState<HTMLElement>(null);
const ref = React.useCallback((nodeElement: HTMLElement | null) => {
setNode(nodeElement);
}, []);
// Render
return (
<div ref={ref}>...</div>
);
In the code example above, we create a new state variable called
`node`
and set it to `null`
. We also define a callback function called `ref`
that takes a single argument representing the DOM node of our target element.To make sure that the
`ref`
callback is only created once and doesn't cause unnecessary re-renders, we wrap it with `React.useCallback()`
. Whenever the `ref`
callback is invoked with a new node, we update our component's state by calling `setNode(nodeElement)`
.Using the
`ref`
attribute, we pass our memoized `ref`
callback to our target element. This allows us to access its corresponding DOM node and perform additional actions based on its size or position.By storing the node as an internal state variable, we can easily reference it throughout our component without having to rely on callbacks or other workarounds. This makes it simpler to build more complex and dynamic components that respond to changes in their containers.
For example, we can use the
`useEffect`
hook to track changes in the `node`
state.tsx
React.useEffect(() => {
// Do something with `node`
}, [node]);
The
`useEffect`
hook needs a function as its first argument and an array of dependencies as its second argument. In our case, we want to call our function every time `node`
changes. This way, we can perform some action using the information obtained from the callback ref.Let's revisit the container query in the previous example and update it with our new approach.
tsx
React.useEffect(() => {
if (!node) {
return;
}
const resizeObserver = new ResizeObserver(resizeCallback);
resizeObserver.observe(node);
return () => {
resizeObserver.disconnect();
};
}, [node]);
In this example, we've streamlined the process of creating a
`ResizeObserver`
instance and using it to track changes in the size of a node. We've moved this logic inside the `useEffect()`
hook and included a function that stops tracking size changes by calling the `disconnect()`
function of the `ResizeObserver`
instance.To see this in action, check out the demo below. Try dragging the element on the right side of the screen to see how the number of columns adjusts dynamically. As you move the element left or right, the size of the container changes, updating the layout of the page accordingly. Give it a try!
#The benefits of storing ref nodes as state
Storing the ref node as state is an incredibly useful pattern. By separating common logic into a separate hook, we can easily reuse it later on. For example, let's consider a container query scenario. We can create a custom hook that returns a callback ref and the element's width using the ref.
Here's an example of what the hook could look like:
tsx
export const useWatchSize = () => {
const [node, setNode] = React.useState<HTMLElement>();
const [width, setWidth] = React.useState(0);
const resizeCallback = React.useCallback((entries) => {
entries.forEach((entry) => {
const rect = entry.target.getBoundingClientRect();
setWidth(rect.width);
});
}, []);
const ref = React.useCallback((nodeEle: HTMLElement | null) => {
setNode(nodeEle);
}, []);
React.useEffect(() => {
if (!node) {
return;
}
const resizeObserver = new ResizeObserver(resizeCallback);
resizeObserver.observe(node);
return () => {
resizeObserver.disconnect();
};
}, [node]);
return [ref, width];
};
There's nothing fancy here, except that we've streamlined the process of managing node and width states from the component above to a new hook. The best part is, we can now use this hook for other useful purposes too!
tsx
const [ref, width] = useWatchSize();
return (
<div ref={ref}>
<div
style={{
columnCount: width < 200 ? 1 : width < 400 ? 2 : 3,
}}
>
...
</div>
</div>
);
Here is a demo showcasing the benefits of creating and reusing the hook we've developed.
#Conclusion
After exploring how to use a callback ref and component state to track changes in the size of an element, we can conclude that this pattern is a game-changer for building dynamic and responsive components.
By storing the node as a component state, we can easily reference it throughout our component without having to use complex callbacks or workarounds. Plus, by creating a custom hook to manage the node and width states, we can reuse it across multiple components, making our code more modular and easier to maintain.
Questions? 🙋
Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Recent posts ⚡
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn