A t work this week, I found myself tasked with developing something that sounded fairly simple: A React web component that accepts the name of an icon, and returns an inline SVG of that icon. Such that I could write this:
<Icon name="exampleIcon" />
And it would return this:</pre>
As is the case with, oh I don’t know, say, ⅔ of all simple-sounding dev tasks, it turned out to be not a simple dev task at all. Maybe even the opposite of a simple dev task. And it took me enough Googling and gnashing of teeth that I feel compelled to document the solution I developed, so that if I ever need to do it again I’ll have a handy reference, and also in the event someone else finds their way to this post under similar circumstances they might get a few questions answered.</pre>
1. The Setup
The setup is pretty standard React-web-app-in-2016 fare. We’re using React, and Webpack, and all the typical Node, ES6, and JSX hipster stuff that comes with it. Our icon component, aptly named Icon, is boiler plated like so:
const Icon = (props) => ( <svg className={ props.className } viewBox="0 0 72 72" width={ props.width } height={ props.height } > { props.title &&{ props.title } } /*** Inline SVG data should go here ***/ </svg> );
Yes, we could’ve simply used <img src="exampleIcon.svg">
and inserted the SVG that way, but we wanted inline SVGs for a number of (quite valid) reasons.
Yes, we could’ve imported an object with SVG data, or a bunch of modules that exported SVG data, but we wanted to be able to simply drop in raw SVG data to a folder somewhere and not worry about writing any code or running any generators.
And before you ask, no, we didn’t want to give up the boiler plate <svg>
tag attributes, we wanted those four big beautiful attributes (className
,viewBox
,width
,height
) to be applied just like that, to all SVGs coming through this pipeline.
Okay, so what’s our first step here?
2. The Wrong Approach
My initial line of thought was let’s just do some kind of import *.svg
or something! That’ll import all the SVGs we dropped into the icon directory. But of course it isn’t that easy. Here are a few things that absolutely don’t work:
/*** Doesn't work, because ES6 won't batch-import .svg ***/ import * as iconObject from './icons'; import * as iconObject from './icons/*'; import * as iconObject from './icons/*.svg'; //** Doesn't work, because import has to come first **/ $listOfSVGs.forEach(svg => import svg) /*** Also doesn't work, don't ask me why ***/ const icons = require('./icons') const icons = require('./icons/*.svg')
My next big stupid idea was to use NodeJS, or some other Node plugin, to do the filesystem magic, and return that precious list of SVGs in the icon directory. Then maybe we could require()
them one at a time. “Now I got you, suckers!” I stupidly said to myself, to the SVGs.
First up was my attempt to use the "fs" filesystem plugin that comes standard with Node:
const fs = require('fs'); iconData = fs.readFileSync("icons/*.svg", "utf8")
This fails miserably: "Module not found: Error: Can't resolve 'fs' in '/app/icons'"
. Well, okay, turns out you need to make a space for the "fs" module in your Webpack config. So I did that, and it led me to maybe the most frustrating, misleading error I’ve seen in my life:
Uncaught TypeError: fs.readFileSync is not a function
Uncaught TypeError: fs.readFileSync is not a function
. Read over those words and seethe. Why is readFileSync
in all of the fs documentation if it doesn't actually exist? Why doesn’t this fail until I run it in a browser, and not at a build level? Why?? This is hell, my friends. This is hell.
I’ll spare you the dramatics and cut to the chase. It turns out I had an understanding of the entire concept of React, Node and Webpack that I’d now categorize as "deeply flawed" in this regard. I was treating these tools, the very things stitching together my files to display in a browser, as if they were exclusively back-end tools. They aren’t. Node, and my "fs" function, will only have access to the filesystem when the server is running it. When the browser is running it (or, more precisely, when the browser is running what Webpack has stitched together), it has no such filesystem access (for fairly obvious reasons). In some respects, what I was trying to do is the whole point of Webpack, or Browserify, or any of those kinds of tools. You can’t use Node to do filesystem work for your browser in this fashion.
Anyway, it took me much longer than this one paragraph to understand that, and I tried browserify-fs and rbfs and a few other things to try and get around it, but they were either too much overhead or just the wrong tool for the job. The correct tool for the job is going to be Webpack’s implementation of dynamic requiring. Because Webpack is what’s doing the filesystem work for the browser, dummy! I finally realized to myself.
3. Setbacks and Common Errors
The first problem you’ll run into is that Webpack just chokes real hard on the whole concept of an .svg file. An error like this will pop up: "Module parse failed: /app/icons/svg/icon.svg Unexpected token (1:0) ... You may need an appropriate loader to handle this file type."
That’s because if we’re going to handle a bunch of SVGs like raw data, we’ll need to parse them like raw data. In your Webpack config, add this in the appropriate place:
module: { loaders: [{ { test: /\.svg$/, loader: 'raw' } }] }
Of course, since nothing is free, you’ll need to use NPM or similar to install the "raw-loader" part. For most setups it’s as simple as:
$ npm install raw-loader --save
I also spent too long attempting to do all of the looping, parsing, and logic in the React component code itself. There’s no need to do that, and JSX is actually really awful for that (likely by design), so I’d recommend just offloading all the hard work to a new file, and using a conventional require()
to import it. You’ll see what I mean in the next bit.</pre>
4. The Solution
Here’s what I came up with, and, against all odds, it actually works. I’ll explain line-by-line after the jump:
/** `Icon.component.js` **/ const iconSVGData = require('../icons/index.js'); const Icon = (props) => ( <svg className={ props.className } viewBox="0 0 72 72" width={ props.width } height={ props.height } > { props.title &&{ props.title } } <g dangerouslySetInnerHTML={ { __html: iconSVGData[props.name] } } /> // This is the important bit </svg> );
/** `icons/index.js` **/ const iconRequireObject = require.context('./svg', true, /.*\.svg$/); // require all .svgs in the ./svg folder const exports = module.exports = {}; const iconList = []; iconRequireObject.keys().forEach(path => { const iconName = path.substring(path.lastIndexOf('/') + 1, path.lastIndexOf('.')); // find the bare name of the icon exports[iconName] = iconRequireObject(path); iconList.push(iconName); }); exports.iconList = iconList;
<!-- icons/svg/exampleIcon.svg (example SVG path) --> <d path="1a,2b,3c,[...],1a,2b,3c">
So there you have it. What does it all mean? Here’s a line-by-line breakdown:
Icon.component.js
In the first file, we simply require()
the second file (line 2), and, within the component, add a <g>
tag with the raw SVG data (line 12). Notice the dangerouslySetInnerHTML
feature, which the React developers purposely crafted to sound scary. Mainlining raw code into your component’s veins is generally frowned upon but in this instance, it’s totally fine. I threw it into a <g>
tag, just because it was simpler than factoring in the <title>
tag to live as its sibling (if you inject innerHTML there can’t be anything else living there already). You’ll notice I used iconSVGData[props.name]
to retrieve the raw data. So that means our second file will have to export that data in that format.
icons/index.js
In the second file, line 3, we use "require.context()" to get a list of SVG files sitting on the filesystem. It also does the actual importing of those files, but we’ll have to use it like a function to return that data (see line 9). Lines 4 and 5 are just setting variables, and line 7 just loops over the names of the files. So all we have to do is get the raw icon name (line 8), make sure we’re exporting it properly (line 9), and then add it to a list (lines 10 and 13). Adding it to the list is actually optional, but I needed it for a style guide app we run (long story), so you can safely ignore that.
icons/svg/exampleIcon.svg
This is just any ol’ SVG, but without the <svg>
tag. Path info only! Therefore, the final Icon component just needs to be built like this:
<Icon name="exampleIcon">
And we’re done! The component uses the name we supply to get the icon data of the same name from the icons.index.js
exports! It’s front-end magic.
5. Conclusion
Now, with this setup, all we have to do is drop any number of SVG paths into the icons/svg
directory, and our app will instantly know about it and be able to inject it, based on its filename, into the Icon component. I understand this isn’t a very advanced solution, or a particularly elegant one, but I wasn’t able to find a good example of this on any of my normal channels, so hopefully this will be helpful to someone else trying to do the same thing.
Good luck out there, kids. You’ll need it.