In my previous post about managing SVG icons in React, I discussed a method for dynamically controlling important SVG properties such as color, aria-roles, and size. This involves using a single-source React component to handle all the related SVG icons in our app.
In this post, we will explore how to build an React library for icons using the higher-order component pattern in React (HOC). This is a simplified approach that is also used by material-ui and fluent ui libraries and is heavily tested.
Disclaimer: Design systems in the scale of MaterialUI or FluentUI use scripting and automated processes to handle SVG icons, including downloading those icons from CDN and automatically generating icon factories.
This approach provides additional control over the SVG icons. Some of the key benefits will be discussed below.
Consistency: Every icon we generate through the factory is wrapped in the same svg
element and shares the same properties.
Build time control: Controlling the generation of SVG icons during the build time can provide better performance optimization and a more secure control compared to generating them at runtime.
Testing SVG components: We optimize the size of our snapshots while retaining important data for our tests.
Final Stats: We compared the impact of the SVG factory approach.
Cons: Of course, this approach is not suitable for all use cases, and we ought to look into alternative solutions based on the challenges that we have to tackle.
Conclusion: My final thoughts regarding SVG factory approach and other alternatives.
Consistency
✅ Let's begin constructing our SVG factory. First, let's create a wrapper component that will encapsulate our icons.
const SvgIcon: React.FC = ({ children }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
>
{children}
</svg>
);
};
❕This component will eventually wrap each of our existing and upcoming SVG icons, ensuring uniform behavior across all of them.
Usage example:
const CloudIcon = () => {
return <SvgIcon>
<path d="M3 18H7M10 18H21M5 21H12M16 21H19M8.8
15C6.14903 15 4 12.9466 4 10.4137C4 8.31435 5.6
6.375 8 6C8.75283 4.27403 10.5346 3 12.6127
3C15.2747 3 17.4504 4.99072 17.6 7.5C19.0127
8.09561 20 9.55741 20 11.1402C20 13.2719 18.2091
15 16 15L8.8 15Z" stroke="#000000" stroke-
width="2" stroke-linecap="round" stroke-
linejoin="round"/>
</SvgIcon>
}
🔛 Now that we have our wrapper component set up, let's take advantage of it and include some properties that should be applied to every wrapped SVG icon.
type Props = {
/* Any valid svg vector */
children: React.ReactNode;
/* Any additional css class the of svg icon */
cssClass?: string;
/* The width of the svg icon */
width: number;
/* The height of the svg icon */
height: number;
/* The viewbox of the svg icon */
viewBox?: string;
};
const SvgIcon: React.FC<Props> = ({
children,
viewBox = '"0 0 24 24"',
width,
height,
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
className={cssClass}
width={width}
height={height}
viewBox={viewBox}
>
{children}
</svg>
);
};
ℹ️ We have included some properties that allow us to adjust the size of each icon during runtime. These properties include the ability to dynamically change the width
and height
, set a default value for the viewport of our SVG icon, and a cssClass
prop for future CSS adjustments. Additionally, the children
prop will hold all the inner structure of the SVG icon.
You can find more information about controlling SVG icons dynamically in my previous post.
Build time control
🛠️ Now, let's proceed to our SVG factory. The factory function we will create is essentially a higher-order component (HOC) that generates a new component with additional properties.
export function generateSvg(
path: React.ReactNode,
displayName: string
): typeof SvgIcon {
const iconName = `${displayName}Icon`;
function Svg(props, ref) {
return (
<SvgIcon id={iconName} data-testid={`${displayName}Icon`} {...props}>
{path}
</SvgIcon>
);
}
if (isProduction) Svg.displayName = iconName;
return Svg;
}
🛠️ Let's generate a memoized version of the component by using React.memo
. This way, the component will not re-render when its props are unchanged.
...
export function generateSvg(...): typeof SvgIcon {
...
return React.memo(Svg);
}
🎉 Now the SVG factory is fully prepared to generate our SVG icon components, ensuring optimal performance and maintaining a consistent structure.
Testing SVG components
🔬 SVG icons can result in larger snapshot files due to the inclusion of their internal XML structure, which is reflected in the component's snapshot file. In these situations, we can omit the vectors from our snap files. One effective method is utilizing the svgr package
. For more in-depth information, you can refer to the documentation found here. Once again, additional dependencies will need to be installed in your app.
ℹ️ In order to address this issue for our case, we can improve our SVG factory to efficiently generate the internal structure of an SVG except test environment. Therefore, it is still possible to keep important data from our snapshot files, such as the icon name or additional properties of our SVGs (e.g. the data-testid
attribute).
const isTestEnv = process.env.NODE_ENV === 'test';
const mockSvgContent = <path />;
...
export function generateSvg(
path: React.ReactNode,
displayName: string
): typeof SvgIcon {
const iconName = `${displayName}Icon`;
function Svg(props, ref) {
const content = isTestEnv ? mockSvgContent : path;
return (
<SvgIcon id={iconName} data-testid={`${displayName}Icon`} {...props}>
{content}
</SvgIcon>
);
}
...
}
ℹ️ Here is an example of a snapshot file with and without the inner SVG structure.
Final Stats
Bundle size using SVG assets and SVGR library.
Total project size: 149.7 kB
Build time: 290 ms
Bundle size using the SVG factory.
Total project size: 148.91 kB
Build time: 273 ms
Overall, the usage of the SVG factory for a single SVG icon has resulted in a reduction of 0.53% in the total bundle size and 5.9% in the build time of the project. Not bad at all 😎!
ℹ️It should be noted that this project is just for demonstration purposes and that the build time or bundle size may differ depending on the configuration of your project.
Cons
The limitations of this approach's use cases, especially in large codebases, are based on our own decisions about how to handle SVG icons. Furthermore, we should highlight that this may not be the best solution for those who want a quick way to add custom SVG icons to their project, and they should definitely use one of the solutions listed above or in my previous post (such as the SVGR
package or img
html tags).
Conclusion
Demo repository: https://stackblitz.com/edit/vitejs-vite-8c4uok?file=src%2FApp.tsx
Usage:
const CalendarIcon = generateSvg(
<path
d="M21 11.5V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22H12.5M21 10H3M16 2V6M8 2V6M18 21V15M15 18H21"
stroke="#101828"
stroke-width="2"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
></path>,
'Calendar'
);
function App() {
return (
<>
<CalendarIcon width={16} height={16} />
<CalendarIcon width={32} height={32} />
<CalendarIcon width={64} height={64} />
<CalendarIcon width={128} height={128} />
</>
);
}
⭐ In summary, we created an SVG factory to manage all of our SVG files during the build process. When possible, we also established a single source of truth for SVG icons, which:
- includes performance enhancements such as
React.memo
. - adds SEO enhancements such as
title
andaria-hidden
. - streamlines the process of testing SVG icon changes and minimizes the size of snapshot files.
While there is no ideal solution, we should select the strategy that best suits our requirements. As previously noted, there are other options that involve using external packages like SVGR, but this strategy might be helpful if your app has a large number of icons and you truly have to control them efficiently.
Useful links
Top comments (2)
142.81 + 1.39 + 4.13 + 0.46 = 148,79
So no, no bundle size decrease this way :)
Besides that, having individual svg icons means each can be cached for a long time since they rarely (ever?) get updated. While the js bundle gets larger with each added svg and changes on each release. To each his own I guess?
I appreciate your feedback, Richard. Thank you. Besides my miscalculation, I still included a note that obviously you have to compare projects with a decent amount of SVG to check the difference; this is just a POC. Additionally, you also have the built-in time gains, which save time and money in your CI.
So to conclude, factories generate code at runtime, not in the server; this is why it is way more efficient, maintainable, and scalable than having static XML markup on individual files (e.g., icon.svg, arrow.svg, etc.). For sure, there may be many more alternatives (deliver SVGs via CDN etc) and to be honest, I don't have a strong opinion on a specific one since it largely depends on each project's needs.
Finally, I want to thank you again for your honest feedback. I would also like to "hear" your alternative approach to handling SVG icons so that others can benefit from it as well. Cheers! :)