Dynamically loading SVG files to build an Icon component with Webpack

Leo Sjöberg • February 5, 2019

Inline SVGs are great – you can manipulate them as you like, modify colours, easily transform them, and they look great in browsers (as opposed to icon fonts). However, in several projects now, in both Angular and Vue, I've found myself either manually importing those SVGs with the Webpack raw-loader through an import statement, one by one, or using a switch in the HTML template to output the right data. The former results in a massive amount of import statements, and an equally large object to map icon names to their loaded contents. The latter results in a single template containing tens of inline SVGs, becoming quite difficult to manage.

To solve this, I decided to make use of Webpack's require.context and a simple if check:

1const context = require.context('!raw-loader!./icons', true, /.svg$/)
2const svgIcons = context.keys().reduce((prev, key) => {
3 const filename = key.substring(key.lastIndexOf("/") + 1)
4 prev[filename.substring(0, filename.length-4)] = context(key)
6 return prev;
7}, {});

This will use the raw-loader to load in an object of the format {filename: 'svgcontent'}. It automatically strips the path and extension from the filename, so we can then simply refer to the actual icon name in the component, e.g

1<icon for="cog"></icon>

Would load the SVG in cog.svg. The icon component could then do something simple like

1if (Object.keys(svgIcons).contains(args.for)) {
2 return <div innerHtml={svgIcons[args.for]} />
5// fallback
6console.warn('Unknown icon requested')
7return <div innerHtml={svgIcons.unknown} />

Note that I'm passing true to the second argument of require.context – this means that it will recursively load files in the directory, which can be useful for structuring your icons if you have loads of them. Keep in mind, however, that filenames would still need to be unique since we strip the path from the names.

It should be noted that you could also do a dynamic require like this:

1return <div innerHtml={require(`!raw-loader!./icons/${args.for}.svg`)} />

While this would work, it doesn't allow you to be as flexible as the require call now must be placed within the Icon component. By loading all icons separately, you could keep that in a const as part of a module and import it, only requiring the data to be stored in one place.

Note that dynamic requires will still gather all files, just like require.context, so there's no performance benefit either.