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 (<svgwidth="100%"height="100%"ref={handleRef}><ChildparentWidth={currentWidth}parentHeight={currentHeight}/></svg>);};
And inside the Child
:
const Child = ({ parentWidth, parentHeight }) => {// let's say we know child's size alreadyconst width = 800;const height = 600;const [posX, posY] = useCenterPosition({ width, height },{ width: parentWidth, height: parentHeight },);return (<svgwidth={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 (<svgwidth="100%"height="100%"ref={ref}><ChildparentWidth={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!