jakub-antolak.dev

moon indicating dark mode
sun indicating light mode

Aligning SVGs in React

January 25, 2020

I've come across a problem of aligning the SVG node nested inside another SVG node where dynamic positioning was a must. The parent node took the sizes from its parent (a div in this case), so the standard SVG width and height were equal 100%. When you tried to getBBox of it, it was returning its child's size. The child node on the other side had to be on the center both, when the page was loaded, and after the user resized the window.

Hook

No matter if you want to align your svg statically or dynamically, you need to create a hook. A hook, which will compute current central point given the sizes of the child and parent nodes.

const useCenterPosition = (childSize, parentSize) => {
const [x, setX] = React.useState(0);
const [y, setY] = React.useState(0);
React.useEffect(() => {
const { width: outerWidth, height: outerHeight } = parentSize;
const { width: innerWidth, height: innerHeight } = childSize;
setX((outerWidth - innerWidth) / 2);
setY((outerHeight - innerHeight) / 2);
}, [childSize, parentSize]);
return [
x, y,
];
};

Then you should make a use of it, obviously. But while you know the child SVG's size, the parent's size remains a mystery.

Static alignment

The refs shall help us. Go one level up and assign a ref function to the parent SVG node. It will report its bounding box' size to the local state, and then pass it down to the child.

const Parent = () => {
const [currentWidth, setCurrentWidth] = React.useState(0);
const [currentHeight, setCurrentHeight] = React.useState(0);
const handleRef = React.useCallback(instance => {
const { width, height } = instance.getBoundingClientRect();
setCurrentWidth(width);
setCurrentHeight(height);
}, [setCurrentWidth, setCurrentHeight]);
return (
<svg
width="100%"
height="100%"
ref={handleRef}
>
<Child
parentWidth={currentWidth}
parentHeight={currentHeight}
/>
</svg>
);
};

And inside the Child:

const Child = ({ parentWidth, parentHeight }) => {
// let's say we know child's size already
const width = 800;
const height = 600;
const [posX, posY] = useCenterPosition(
{ width, height },
{ width: parentWidth, height: parentHeight },
);
return (
<svg
width={width}
height={height}
x={posX}
y={posY}
>
</svg>
);
};

Here's a static demo (try to resize the Result by clicking scale buttons).



Dynamic alignment 💫

Let's make things more fluent. We need to hire more hooks, the resize event and the resize event handler. But first of all, we must replace handleRef with an object.

const Parent = () => {
const [currentWidth, setCurrentWidth] = React.useState(0);
const [currentHeight, setCurrentHeight] = React.useState(0);
const ref = React.createRef();
const handleResize = React.useCallback(() => {
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect();
setCurrentWidth(width);
setCurrentHeight(height);
}
}, [ref, setCurrentWidth, setCurrentHeight]);
React.useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
}
}, [handleResize, currentHeight, currentWidth]);
return (
<svg
width="100%"
height="100%"
ref={ref}
>
<Child
parentWidth={currentWidth}
parentHeight={currentHeight}
/>
</svg>
);
};

What's going on here? We use a ref object. It lets us to read parent's properties inside the handleResize function. This function is being invoked on each global resize event.

Note the usage of useEffect while doing that, and the returning function unbinding the listener after the component unmounts. It protects us from eventual memory leaks and from having the resize event attached to the window object even after the Parent's removal.

But we're missing something. We've achieved one goal, yet we forgot about the page load. Initially, the currentWidth and currentHeight values are set to 0, so the Child SVG element remains in the top left corner. We need to add one small if statement before binding the resize listener:

if (currentWidth === 0 && currentHeight === 0) {
handleResize();
}

Here's a working pen, with Child in the center from the very beggining.



Summary

Positioning SVG elements might be tricky, especially when the parental HTML structure changes its size dynamically. If you add the SVG complexity, and the framework or library specifics on top, things might get much more complicated.

Fortunately, React lets us handle the logic/view separation smoothly, so the final code remains readable and extendable. I hope this article helped you to better understand hooks and refs usage in relation to the SVG files!