Material UI (MUI) library is a well-known React UI tools that help you build UIs in React more easily and quickly. It provides its own set of material icons, but also allows you to use your own SVG icons.
It works perfect when you have s single SVG image. However, certain issues occur when you try to implement using many custom icons from an SVG sprite. An SVG sprite allows you to reduce the number of image requests your site issues, and the total overall bundle size. Ideally, they allow you to use a single file as the source of all your icons for an application. For that reason, it’s definitely worth exploring how to use these two technologies together.
Note: This blog post is devoted to very specific but important issue that almost broke my brain before I came up with an appropriate solution. In the future, we could expand on my solution, and perhaps resolve this issue in an even more elegant way. Until then, below is the solution I came up with.
Steps To Use Custom SVG Sprite Icons in MUI
The steps for implementing SVG sprites in MUI components sounds quite simple and obvious:
- Create the SVG sprite itself, if you don’t already have one in your project
- Create the correct import of the sprite file
- Extract and display required icon in MUI Material components
Creation of SVG Sprite
This should be the simplest part of the plan. The approach totally depends on your preferences, but however you set it up, the internal structure of the sprite should end up looking something like this:
<?xml version="1.0" encoding="UTF-8"?>
<svg ….>
<svg ….>...</svg>
<svg ….>...</svg>
</svg>
It is worth mentioning that NPM package called “svg-sprite” is perfect for this job. To use it, the svg-sprite package should be added as dev dependency in your project. There are a lot of options this sprite builder can started with, you can investigate them all by its official documentation’s linked above.
Among other options, it allows you to specify both source and destination folders, names of files, and build type. For my purposes, the output file can be built as stack, so I used the CLI options for this in my build script
Correct Import of the SVG Sprite File
The official MUI documentation for SVG icon offers us a particular package that can help to resolve SVG’s files correctly while being imported into React component. Please see an example below. It shows clearly how we can manage an import of the SVG file:
// webpack.config.js
{
test: /\.svg$/,
use: ['@svgr/webpack', 'url-loader'],
}
As you can see, this required modification of webpack.config.js file and installation of “svgr/webpack” and “url-loader” as a dev-dependencies. We need all of that stuff for correct resolving of the path to SVG file, whether it will be a single icon or a combined SVG sprite.
Apply the Imported SVG Sprite to MUI Components
The MUI library suggests to use its <SVGIcon /> component as a wrapper for our custom icons. There are several way to implement this as a solution. Examples below are the most common methods:
1. Directly pass imported icon as a prop, like this:
import { ReactComponent as StarIcon } from './star.svg';
<SvgIcon component={StarIcon} />
This is a nice way for a icon that’s a single file.
2. Pass icon’s SVG markup as a children. That can be done with HTML-element <use /> which points onto the path to an icon or sprite file, like this:
<SvgIcon>
<use xlinkHref=’path-to-your-SVG-file’ />
<SvgIcon />
3. There’s a more exotic way that is similar to the option described above. You should somehow extract <path> element from your SVG file and pass it as a child to MUI component:
<SvgIcon>
<path …> // full copy from child HTML element of you SVG file goes
// there
<SvgIcon />
A Working Solution
So let’s create out custom React component as a wrapper for <SvgIcon> that allows us render icon from an SVG sprite in a MUI component. My working code example looks like this:
import React from 'react';
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
import Sprite from ‘path-to-your-sprite’;
interface IProps extends SvgIconProps {
iconName: string;
}
const CustomMUIIcon : React.FC<IProps> = (props: IProps): JSX.Element => {
const { iconName, ...rest } = props;
return (
<SvgIcon {{...rest}}>
<use xlinkHref={`${Sprite}#${iconName}`} />
</SvgIcon>
);
};
export default CustomMUIIcon;
You can customize it using properties from SvgIconProps, and your should point out the name of the icon that is equal to name of icon’s ID in the SVG sprite file.
This solution is easy to implement and works just fine, until…
The Main Pitfall of the <use> Element
You get a result of your icon rendering that is far from expected if the original SVG icon has internal links to be resolved. For example (unnecessary attributes are omitted):
<svg>
<defs>
<path id="path_id" d="…….."/>
</defs>
<g>
<mask id="mask_id">
<use xlink:href="#path_id"/>
</mask>
<g mask="url(#mask_id)">
<path d="……."/>
</g>
</g>
</svg>
That is not an issue of MUI library itself. Browsers do not support masks or clipPaths rendering inside external <use>
elements. You can try it out with plain HTML file and the result will be the same – unpredictable rendering of internal paths.
Using Dynamic Import Instead
The answer is all about using dynamic import to bring in the icon as a React component from the SVG sprite, and using it as a property of the <SvgIcon /> component. It works well with a single file and it resolves internal references correctly, so all your masks and clipPaths display as expected.
My proposed solution would look something like this:
import React, {useState, useEffect} from 'react';
import SvgIcon, {SvgIconProps} from '@mui/material/SvgIcon';
import Sprite from 'path-to-your-sprite';
interface IProps extends SvgIconProps {
iconName: string;
}
const CustomMUIIcon: React.FC<IProps> = (props: IProps): JSX.Element => {
const { iconName } = props;
const [ iconComponent, setIconComponent ] = useState(null);
useEffect(() => {
import(`${Sprite}#${iconName}`)
.then((result) => {
const { ReactComponent as Icon } = result;
setIconComponent(Icon);
})
.catch((error) => {
// do some usefull stuff here
})
}, [ iconName ]);
return (
<>
{ iconComponent && <SvgIcon component={iconComponent} /> }
</>
);
};
export default CustomMUIIcon;
This doesn’t work because it imports the whole sprite file instead of the addressed icons. As a possible solution, I suggest parsing the HTML markup of the imported SVG file, extracting the innerHTML for all the named icons, and injecting it into <SvgIcon />. My main concern is the performance will be unnecessary affected.
Solution – Old but Gold
Let’s inject the SVG sprite on the root level of the application and allow the browser to resolve all internal paths in the wild, so to speak. This solution comes from old approach that allows you to resolve SVG sprites on server-side rendered web-sites. We hide the sprite file with CSS properties. Our custom material icon component just resolve path to icon with a regular href.
const CustomMUIIcon: React.FC<IProps> = (props: IProps): JSX.Element => {
const {iconName, ...rest} = props;
return (
<SvgIcon {{...rest}}>
<use xlinkHref={`#${iconName}`}/>
</SvgIcon>
);
};
export default CustomMUIIcon;
import React from 'react';
import { ReactComponent as Sprite } from 'path-to-your-sprite';
const styles = {
height: 0,
opacity: 0
};
const SvgSprite: React.FC = (): JSX.Element => {
return (
<div style={styles}>
<Sprite/>
</div>
);
};
export default SvgSprite;
Conclusion
The suggested method looks a bit cumbersome, but it works as a good solution, allow you to apply custom icons from an SVG sprite to MUI components in a React application. Also, keep in mind that the solution using the dynamic import of component needs to be investigated with a strict condition to avoid negatively impacting the performance and memory usage.