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:
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:
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.
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.
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:
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!
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.
It's highly recommended that you visit the original post to play with the interactive demos.
If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!
If you want more helpful content like this, feel free to follow me:
Top comments (0)