Dynamic SVG Imports in Create-React-App

Using react-scripts@5.0.1 & react@18.2.0

Ryan Hutzley
10 min readNov 10, 2022
Photo by Lautaro Andreani on Unsplash

The Problem

You have a bunch of SVGs you want to include in your CRA. You want to avoid a gazillion import statements at the top of your React Component file (like below)…

Too many imports!

In this post, I will show you how to dynamically import your SVGs in CRA using a custom hook. First, we’ll dive into the details of how this custom hook works. Then I will explain why using SVGs as React Components is better than using SVGs as images. Next, we will peek under the hood of Create-React-App to 1) inspect the Webpack configuration, 2) break down how Webpack uses svgr (a tool that transforms SVGs into React Components), and 3) implement a quick fix for a Webpack issue that, at the time of writing (November ’22), prevents dynamic imports of SVGs. Finally, I will explain how to implement this solution in production (without ejecting).

NOTE: all of the code I am about to show you can be found in this repo.

The useDynamicSVGImport Custom Hook

This hook comes to us from Kwan Jun Wen’s amazing answer to the question of “How to dynamically import SVG and render it inline?”

Let’s take a look at the code

Kwan Jun Wen’s useDynamicSVGImport hook

This hook contains a loading/error state and an ImportedIconRef (using the useRef hook). Inside the useEffect we have an async function called ImportIcon that is composed of a try, catch, finally block. Inside ImportIcon we try to assign ImportedIconRef.current to the result of the await-ed import statement. Lastly, this hook returns an object containing the error + loading state and SvgIcon: ImportedIconRef.current. At a high level, this hook is using the provided name to import an SVG, which can be rendered inline as a React Component (using some bundler magic).

The magic is happening toward the end of the import statement — see that .ReactComponent? Don’t worry about it just yet, I’ll return to it in the next section. For now, just know that ImportedIconRef.current is being assigned to a React Component.

So now that we have our custom hook, how do we actually use it? This brings us to the Icon component…

Icon Component with useDynamicSVGImport

This component calls our hook, and if the response contains a React Component for the desired SVG, it will return it along with along with the rest of the props passed to it. (For an explanation of rest in React check out What does …rest mean in ReactJS by Manesh Joshi).

Here’s one example of how we can use the Icon in our application…

Icon used in a separate React Component

This is taken from the Footer section of my recent Create-React-App project (check out gagesports.org to see what the full site looks like). We are passing the Icon a few props, the most important being name, className, fill, height, and width (onCompleted and onError are just callback functions that console.log either the name or the error message). The name tells Icon which SVG to render, while the className, fill, height, and width props provide the styling (I’m used Tailwind CSS for this project, hence the funny classes). This is the advantage of rendering the SVGs inline… You can style the SVG directly, by simply passing SVG-specific props to our component (e.g. fill , viewBox , etc.).

A simple and common alternative to inline rendering of SVGs in React is to wrap them in an <img/> tags, like so…

<img src=”path/to/icon.svg” />

This is relatively straightforward, and dynamically importing SVGs can be as easy as

<img src={require(`./icons/${icon.code}.svg`)}/>

However, this simplicity comes at a cost. You lose the ability to customize your SVGs. Attributes like fill, or viewBox are not valid for image elements. Therefore, if customization is a priority for your SVG assets the inline React Component is the solution for you. If not, keep it simple and use the img tag.

useDynamicSVGImport Behind the Scenes

As promised, let’s revisit the .ReactComponent section of our custom hook. Here is the entire line again…

ImportedIconRef.current = (await import(`../assets/icons/${name}.svg`)).ReactComponent;

This code assigns the current property of our ImportedIconRef to the await-ed output from the Webpack import function. To understand why .ReactComponent is necessary, navigate to the webpack.config.js, found in node_modules/react-scripts/config/webpack.config.js.

There’s a lot happening inside this file…

Welcome to webpack.config.js!

Let’s break it down. As a refresher, Webpack is a

static module bundler for modern JavaScript applications… It internally builds a dependency graph from one or more entry points and then combines every module your project needs into one or more bundles, which are static assets to serve your content from.

webpack.config.js is a JavaScript module that exports an object that customizes the behavior of Webpack.

Now that we have a basic understanding of Webpack, it’s time to explore how Webpack handles SVGs in CRA.

Inside webpack.config.js (using react-scripts@5.0.1 & react@18.2.0), if we Command/Ctrl + F and type “svg” we should arrive at this block of code

SVG handling in webpack.config.js

If you zoom out, you will see that this code is an element inside the rules array, which is itself a key inside the modules object. The object structure looks something like this

module.exports = {
...
modules: {
...
rules: [...]
}
}

The modules object “contains options that determine how the different types of modules within a project will be treated.” And each Rule in the rules array matches a specific filetype to a loader, which specifies how Webpack should preprocess a non-JavaScript file (e.g. CSS, JSON,… SVG!). Lastly, we have the issuer which stores “a condition to match against the module that issued the request.” First, Webpack checks whether the file ends in .svg. Next, Webpack uses two loaders to preprocess the SVG. Then, Webpack verifies that the issuer is valid.

Now back to the SVG handler…

svgr

Webpack uses svgr (or more precisely, @svgr/webpack) as a loader for SVGs. svgr is “a universal tool to transform SVG into React components.” The svgr documentation tells us why we need the .ReactComponent in our custom hook:

By default, @svgr/webpack will try to export the React Component via default export if there is no other loader handling svg files with default export. When there is already any other loader using default export for svg files, @svgr/webpack will always export the React component via named export.

In our webpack.config,js we observed that Webpack uses both @svgr/webpack and file-loader to preprocess our SVGs, meaning that our React Component is not exported via default export, but rather via named export.

We can verify that this is the case by logging our imported module both with and without file-loader. When both loaders are used, our module looks like

React Component as a named export (accessed via ReactComponent property)

After commenting-out the object containing file-loader and restarting the server, the module becomes

React Component as default export

Webpack Issue Fix

Congrats on making it this far! You now know how CRA handles SVGs under the hood 🛠 But if you’ve been following along in your own application, you likely ran into the issue where .ReactComponent in the useDynamicSVGImport hook returns undefined. This is due to a Webpack issue where the @svgr/webpack loader is not applied to dynamic SVG imports. The workaround is to simply comment out the issuer key-value pair. Now the SVG section of your Webpack configuration should look like the code below

If you save this change and restart your application, you should be good to go. That is, until you deploy your app…

Implementing the Fix in Production

You got dynamic SVG imports up and running and pushed your code to a remote repo. Now you want to deploy your app so that the whole world can see your awesome site. You import your remote repo to a site like Netlify or Heroku (or another provider listed here). The deployment is successful, time to break out the champagne! 🍾 But wait… “where are my SVGs?” None of them are showing up 🙁

Here’s the reason why. The Webpack issue workaround is not reflected in the remote repo. The node_modules directory is .gitignore-d by default, and with good reason — CRA “encapsulates all of the npm modules it uses internally, so that your package.json will be very clean and simple” (thanks casieber). Our package.json is the tip of the CRA iceberg. Therefore, in order to customize the behavior of Webpack (a.k.a how our application actually gets built/bundled) we need to expose the whole iceberg. This is what’s known as ejecting.

CRA documentation explains that running npm run eject

will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc.) into your project as dependencies in package.json

This is a one-way operation. There’s no going back. This isn’t necessarily a bad thing. You are left with a lot more code to maintain, but in return you get complete control.

I would argue that most developers would rather avoid having to maintain all of this configuration. Therefore, I will explain how to implement our Webpack fix without ejecting.

Overriding the CRA Webpack Configuration Without Ejecting

While there are a few alternatives to ejecting (see CRA docs), I have found François Zaninotto’s chosen method to be the simplest (check out his blog on this topic here).

The first step in this process is to npm install -D rewire. This adds the rewire package to our devDependencies in package.json. rewire is a general-purpose Node.js monkey-patching library.

Next, we create a build.js file in the root of our application. This will contain a node script that overrides the Webpack build.

build.js code

Using rewire, we can access and overwrite the default Webpack configuration through the config object. By dumping the entire config into console we can systematically drill into the object until we’ve reached the part we want to override. In our case, the part we want to override is located at config.module.rules[1].oneOf[2]. To implement our Webpack fix we simply set config.module.rules[1].oneOf[2] equal to its default content minus the issuer key-value pair.

BONUS: In the last two lines of our build file, I went ahead and overrode the “Component Name Mangling in Production” setting (this is the main focus of Zaninotto’s blog). By default, Webpack mangles React Component names in production to minimize the bundle size. This means that in React DevTools, your components will either appear as “Anonymous” or a few random letters. With this custom configuration, we can access full component names in our production application (go check out gagesports.org and open React DevTools + Components to see all of my custom component names).

The last step to fully implement the Webpack fix in production is to change the build script in our package.json.

"scripts": {
"start": "react-scripts start",
- "build": "react-scripts build",
+ "build": "node ./build.js",
"test": "react-scripts test",
"eject": "react-scripts eject"
},

At last, our application is ready for production! Now you can break out that bubbly 🥂

Conclusion

I hope you enjoyed learning about dynamic SVG imports in Create-React-App. We covered a lot of territory — from unpacking the nuances of svgr to defining eject in CRA. I realize that things can get complicated quickly once we start exploring CRA internal processes + Webpack, so if you feel that this blog helped demystify any of these under-the-hood operations — mission accomplished! 🚀

Regarding the current issue with Webpack not handling dynamic imports appropriately, the previously outlined workaround seems like the best solution until the problem is resolved — at least if you are committed to using Create-React-App. Personally, this experience of having to debug Webpack issues has motivated me to utilize other tools like Vite for my next React application. What about you? Are you planning on ditching Create-React-App anytime soon?

I’d love to hear your thoughts, comments, feedback, etc. Feel free to drop a comment on this article, send me an email (ryanhutzley@gmail.com), or DM me on LinkedIn.

Don’t forget to check out my most recent project: gagesports.org. And if you’re interested in football or sports betting go give my brother Gage Hutzley a follow.

Thanks for reading! 🙂 Below you’ll find some useful resources when working with SVGs in Create-React-App.

Resources

And if you need a Frontend Developer… look no further! I am available to help you make your idea into a fully functional React Application. For business inquiries, please email me at ryanhutzley@gmail.com.

Looking forward to connecting!

--

--

Ryan Hutzley

Princeton University '20, B.A., Flatiron School Software Engineering Graduate