So, for some reason you have a SVG file that somehow resembles a part of the application you are currently developing.
This might be only a sketch that you want to prepare as a Click Dummy, or it might be that you need to display a somewhat unique, complicated structure that is best layed out per SVG editor. This is somewhat expected when you have customers in the technical / scientific research sector.
So now you want to fill it with life.
Now, the SVG format is quite close to the <svg> structure that one can embed into HTML, but there are some steps in between. Most importantly, most SVG Editors fill their .svg files up with meta data or specific information only required for the editor in case you want to edit the files again.
Thus, you have three choices to integrate the SVG component in a React application
- Re-Build your SVG with custom React Components that, via JSX, render their <svg>, <g>, <path>, etc. accordingly
- Convert your SVG to valid JSX – this is possible in many cases, but you need to take care e.g. that the style attribute is a string in the SVG and an object in JSX, also it can be still way too large to be readily put in a single React Component
- Import your .svg as its own React Component and then wrap that into an own Component that takes care about the interaction part
While I also have written a small converter that does the SVG-JSX-Conversion just fine for me for any file that comes out of Inkscape (probably an idea for my next blog post), we had the case of some files with about 16000 lines of SVG each, so I chose the third method in our case.
In my eyes, it is very correct to mention that the following way somehow goes against the React Mindset. In which you render all your components yourself to attach them the required mouse event handlers, never having to interact with a HTML “id” or any document.getElementById() or document.getElementByClassName().
In any React application, these should be avoided, but the idea here is to have a singular point – a SvgWrapper Component – where you allow these functions because you’d agree about the need to somehow target the specific SVG elements.
The gist:
import {ReactComponent as OurHorrificSvgMonster} from "/src/monster.svg";
const OurBeautifulComponent = () => {
useOurCarefulSvgSynchronizationEffect(); // more on this below
return (
<SvgWrapper>
<OurHorrificSvgMonster/>
</SvgWrapper>
)
};
Quick note: you can target the embedded <svg> element itself by <OurHorrificSvgMonster ref={...}/> and you could use this (ref.current holds that HTML element) to traverse all the children, so if you know much about the structure of your svg you could even live without the <SvgWrapper>. But say someone else made the horrific svg monster and all you have is the id or class names to all the individual svg elements inside.
Then
const WithVanillaHandlersConnected = ({children}) => {
const dispatch = useDispatch();
React.useEffect(() => {
const onClick = (event) => {
dispatch(awesomeAction(event.target.id));
};
const awesomeElements = [...document.getElementsByClassName("awesome")];
awesomeElements.forEach(elem => {
elem.addEventListener("click", onClick);
});
return () => awesomeElements.forEach(pipe => {
pipe.removeEventListener("click", onClick);
});
}, []);
return children;
};
I chose this dispatch() as a placeholder for any interaction with the surrounding web application, it could also be a simple React state or something. You can register any event listener you want here (also “mouseover”, “mouseout”, “contextmenu”, …), but think of removing it again in the effect return function.
By the way, document.getElementsByClassName(…) returns something like a HTMLCollection which is not exactly iterable, thus the […destructuring] to make the .forEach() possible.
We now have the first part – our elements (in our case, everything that has class “awesome”) has got a click handler that allows to dispatch anything to the application state. But now they need to change, too.
In a purely React-y way, this could be done by a svg element that chooses its fill = {isActive? "magenta" : "black"} but as we choose not to render our components ourselves, we need to once again grab deeply into the DOM and dare to manipulate it by hand.
As mentioned above – this is a step towards very ugly problems as React cannot guarantee that your visual layer matches your application state. You, on yoru own, have to guarantuee to do what’s right.
This is where this comes in:
/*
for this example, think of that the redux selector selectSomethingFromTheState returns something like:
result = [
{elementId: "elem1", isActive: true},
...
];
and isActive could be the thing that was toggled by our awesomeAction() above
*/
const useOurCarefulSvgSynchronizationEffect = () => {
const elementStates = useSelector(selectSomethingFromTheState);
React.useEffect(() => {
for (const state of elementStates) {
const element = document.getElementById(state.elementId};
element.style.fill = isActive ? "magenta" : "black";
// ... do other stuff with the DOM element
}
}, [elementStates]);
};
There we have it – we have the SvgWrapper and the use…SynchronizationEffect() that both stray from the React mindset by accessing the DOM directly, but we do it in a fashion where it is concisely encapsulated inside <OurBeautifulComponent> and there is no direct knowledge about the IDs inside the SVG, or Class manipulations, CSS Selectors, etc. elsewhere.
In my opinion, one can indeed go against the rules if it’s necessary, but I also see the option for a Stockton Rush quotation here.. so, if you know of any more elegant way, please feel free to share.
PS: by the way, if you use vite, you might get an “Uncaught SyntaxError” when trying import { ReactComponent ... } – I’ve written about this before.