Dynamic SVG Imports in Create-React-App
Using react-scripts@5.0.1 & react@18.2.0
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)…
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
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…
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…
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…
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
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 loader
s 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
After commenting-out the object containing file-loader
and restarting the server, the module becomes
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.
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!