How to dynamically import SVG and render it inline
Solution 1
You can make use of ref
and ReactComponent
named export when importing SVG file. Note that it has to be ref
in order for it to work.
The following examples make use of React hooks which require version v16.8
and above.
Sample Dynamic SVG Import hook:
function useDynamicSVGImport(name, options = {}) {
const ImportedIconRef = useRef();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const { onCompleted, onError } = options;
useEffect(() => {
setLoading(true);
const importIcon = async () => {
try {
ImportedIconRef.current = (
await import(`./${name}.svg`)
).ReactComponent;
if (onCompleted) {
onCompleted(name, ImportedIconRef.current);
}
} catch (err) {
if (onError) {
onError(err);
}
setError(err);
} finally {
setLoading(false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return { error, loading, SvgIcon: ImportedIconRef.current };
}
Sample Dynamic SVG Import hook in typescript:
interface UseDynamicSVGImportOptions {
onCompleted?: (
name: string,
SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined
) => void;
onError?: (err: Error) => void;
}
function useDynamicSVGImport(
name: string,
options: UseDynamicSVGImportOptions = {}
) {
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
const { onCompleted, onError } = options;
useEffect(() => {
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
ImportedIconRef.current = (
await import(`./${name}.svg`)
).ReactComponent;
onCompleted?.(name, ImportedIconRef.current);
} catch (err) {
onError?.(err);
setError(err);
} finally {
setLoading(false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return { error, loading, SvgIcon: ImportedIconRef.current };
}
For those who are getting undefined
for ReactComponent
when the SVG is dynamically imported, it is due to a bug where the Webpack plugin that adds the ReactComponent
to each SVG that is imported somehow does not trigger on dynamic imports.
Based on this solution, we can temporary resolve it by enforcing the same loader on your dynamic SVG import.
The only difference is that the ReactComponent
is now the default
output.
ImportedIconRef.current = (await import(`!!@svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
Also note that there’s limitation when using dynamic imports with variable parts. This SO answer explained the issue in detail.
To workaround with this, you can make the dynamic import path to be more explicit.
E.g, Instead of
// App.js
<Icon path="../../icons/icon.svg" />
// Icon.jsx
...
import(path);
...
You can change it to
// App.js
<Icon name="icon" />
// Icon.jsx
...
import(`../../icons/${name}.svg`);
...
Solution 2
Your rendering functions (for class components) and function components should not be async (because they must return DOMNode or null - in your case, they return a Promise). Instead, you could render them in the regular way, after that import the icon and use it in the next render. Try the following:
const Test = () => {
let [icon, setIcon] = useState('');
useEffect(async () => {
let importedIcon = await import('your_path');
setIcon(importedIcon.default);
}, []);
return <img alt='' src={ icon }/>;
};
Solution 3
I made a change based on answer https://github.com/facebook/create-react-app/issues/5276#issuecomment-665628393
export const Icon: FC<IconProps> = ({ name, ...rest }): JSX.Element | null => {
const ImportedIconRef = useRef<FC<SVGProps<SVGSVGElement>> | any>();
const [loading, setLoading] = React.useState(false);
useEffect((): void => {
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
// Changing this line works fine to me
ImportedIconRef.current = (await import(`!!@svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg`)).default;
} catch (err) {
throw err;
} finally {
setLoading(false);
}
};
importIcon();
}, [name]);
if (!loading && ImportedIconRef.current) {
const { current: ImportedIcon } = ImportedIconRef;
return <ImportedIcon {...rest} />;
}
return null;
};
Solution 4
One solution to load the svg dynamically could be to load it inside an img using require, example:
<img src={require(`../assets/${logoNameVariable}`)?.default} />
Related videos on Youtube
Majoren
Updated on July 09, 2022Comments
-
Majoren almost 2 years
I have a function that takes some arguments and renders an SVG. I want to dynamically import that svg based on the name passed to the function. It looks like this:
import React from 'react'; export default async ({name, size = 16, color = '#000'}) => { const Icon = await import(/* webpackMode: "eager" */ `./icons/${name}.svg`); return <Icon width={size} height={size} fill={color} />; };
According to the webpack documentation for dynamic imports and the magic comment "eager":
"Generates no extra chunk. All modules are included in the current chunk and no additional network requests are made. A Promise is still returned but is already resolved. In contrast to a static import, the module isn't executed until the call to import() is made."
This is what my Icon is resolved to:
> Module default: "static/media/antenna.11b95602.svg" __esModule: true Symbol(Symbol.toStringTag): "Module"
Trying to render it the way my function is trying to gives me this error:
Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
I don't understand how to use this imported Module to render it as a component, or is it even possible this way?
-
wederer about 4 yearsDoes your svg display correctly if you statically import?
-
Majoren about 4 yearsYes! If I do a regular
import MyIcon from './icons/myicon.svg'
I can render it like<MyIcon />
. -
junwen-k about 4 yearsYou might have to store the resolved SVG in a state instead.
-
Majoren about 4 years@dev_junwen Correct, but storing it in state still doesn't enable me to render it as an inline svg.
-
junwen-k about 4 yearsOr another rather "dynamic" way is maybe you can define a map of name to SVG components, then use the bracket notation syntax
iconMap[name]
to retrieve the correct SVG. I haven't tested it yet but I think that could work. You will need to import all SVG in that case and assign it to the map. -
Majoren about 4 years@dev_junwen Yes, that's how I first did it. Problem is, you would have to manually import all icons as
import CarIcon from 'car.svg'
etc, then map as{car: CarIcon}
, then you can render them inline likeconst Icon = nameToIcon[name]; <Icon />
. But it doesn't solve the problem of dynamically importing all the SVGs.
-
-
Majoren about 4 yearsThis probably works for an img tag where the source will be importedIcon.default, as you wrote, but it doesn't work in my case with an inline svg, where I want to render it as <Icon />. I tried your approach with an async useEffect, but then I need to
setIcon
with the whole Module, not importedIcon.default (the file path), then rendering it like <Icon />, and it gives me the errorElement type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
-
Enchew about 4 yearsWhy do you need to render it as an
<Icon>
? What does this approach give you and is there any difference, that can't be overcome by using<img>
-
Majoren about 4 yearsHow would I then change the properties of the svg if it's a img tag, like the fill color of the icon? Is it possible with an img tag?
-
Majoren almost 4 yearsWow! Thank you, very clean! I had completely missed the ReactComponent in any documentation. I would think it would show up as a public method on the imported object when inspected, imho, but I guess that's not how things work here. Appreciate the help!
-
Majoren almost 4 yearsBtw @dev_junwen ReactComponent never worked for me, I have no idea how this code works in your CodeSandbox, but
.ReactComponent
just returns undefined for me. I hade to change it and use.default
as in @Enchew's example below. I found some great documentation on this here if you go down to "Importing defaults": developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… -
junwen-k almost 4 yearsWhat React version are you using? The sandbox example should be the latest. You can try
import { ReactComponent as Icon } from 'SVG_PATH.svg';
and see if it resolves to the SVG html -
Willege almost 4 yearsFantastic use of
ref
! How would this look in Typescript? -
junwen-k almost 4 years@Willege I've updated the answer. Feel free to take a look at the code sample.
-
miu almost 4 years@dev_junwen Unfortunately this solution doesn't work me. I've tried everything. Even if I download the code from CodeSandbox and install all dependencies from scratch with
npm install
, the SVG images are not loaded. I also don't get an error message. Is there something I'm doing wrong (maybe webpack, babel or typescript configuration)? -
junwen-k almost 4 years@mamiu Are you using Create React App ? If not you will need to setup inline svg loader manually. This answer might help.
-
miu almost 4 years@dev_junwen Thanks for your quick response! I tried it with and without CRA. But still not working. I'm following error in the console, but I think it's unrelated:
Manifest: Line: 1, column: 1, Syntax error.
I tried it first in my main react project, which is not using CRA, but now I created multiple plain react projects (with and without CRA, with yarn and with npm) and have no clue why it's not working. -
junwen-k almost 4 years@mamiu Is there a way I can inspect your project code (setup) ? Or maybe a very simple project that you've tested but doesn't work. There are many possibilities why it doesn't work so Its hard for me to debug without looking at some source codes. Make sure your project actually includes SVG files for you to import and test.
-
Salet almost 4 years@dev_junwen this solution doesn't work for me either. It resolves correctly when I import statically like
import { ReactComponent as Icon } from 'SVG_PATH.svg'
, but the dynamic import like in your answer gets ignored by webpack for some reason. I'm on React 16.3.1 and React-scripts 3.4.1 -
junwen-k almost 4 years@Salet It appears to be a webpack config bug as described here. There are no solutions for now unfortunately.
-
Nicolás Fantone over 3 yearsEffects can't host an
async
function. They return a promise which gets invoked as a cleanup function. robinwieruch.de/react-hooks-fetch-data -
Hoon over 3 yearsRegardless of what the original question is, this probably is the best answer for "dynamically import local files". This works well when I have a list of file URLs and load dynamically.
-
Mark about 3 yearsI'm using CRA and am getting this error
index.tsx:134 Uncaught (in promise) Error: ChunkLoadError: Loading chunk 28 failed.
Does anyone have any insight into what could be causing this? -
surjit about 3 yearsdynamic import not working for me. I get
Cannot find module '.../path/to/file.svg'
. However direct import worksimport {ReactComponent as Icon} from '../path/to/file.svg'
. What am I doing wrong?? -
junwen-k about 3 years@surjit I think you accidentally wrote triple dots
.../
instead of../
. -
surjit about 3 years@junwen-k sorry for the typing mistake while commenting.. but in my code I have
../
-
surjit about 3 years@junwen-k This is the exact error
index.js:1 Error: Cannot find module '../assets/images/sideNav/overview.svg'
-
junwen-k about 3 years@Mark Does restarting your React app helped? Hard to tell with just that error code.
-
surjit about 3 years@junwen-k I tried restarting the app. No effect 😕
-
surjit about 3 years@junwen-k I'm using Create-react-app typescript: 4.2.2, react: 17.0.1. Do you think it might be anything related to webpack??
-
junwen-k about 3 years@surjit I've updated the answer with your current case, hopefully that helps.
-
surjit about 3 years@junwen-k Yes its working 🙂. giving your answer an up vote.
-
Jasur Kurbanov about 3 yearsCan I use your code for personal and commercial projects ?
-
Augustin Riedinger about 3 yearsI have a
Can't perform a React state update on an unmounted component.
if the parent component re-renders before icons finished loading. Any hint on this? Can't those be cancelled? -
junwen-k about 3 years@AugustinRiedinger Is it possible to provide a minimal reproducible sandbox? Perhaps you can wrap the SVG fetch method as a promise and reject the promise immediately on unmount.
-
Augustin Riedinger about 3 yearsYup, I did this:
let isActive = true; if (isActive) {setLoading(false);} return () => {isActive = false;}
(sorry for ugly formatting). Maybe this is worth updating your answer. -
blueprintchris over 2 yearsI get a completely different error:
Cannot read property 'dispose' of undefined
-
nulldroid over 2 yearsPlease help, for me it always logs "svgname.svg successfully loaded", but nothing is displayed, no error message at all. I'm using exactly your code from codesandbox.io. I forked it and updated to react/ react-dom 17.0.2. It's working fine on codesandbox, but not in my current project, which uses webpack. I've tried everything and it should definitely work. The problem is not the svg file itself. I uploaded mine to codesandbox and it works. I even created a new local project with create-react-app. Still, "successfully loaded", no error, but svg is not displayed.
-
nulldroid over 2 years@junwen-k directly downloaded your codesandbox files. Same issue. npm --version 7.20.6 , node --version v14.16.0. I don't get it.
-
junwen-k over 2 years@Nulldroid Does following this solution helps? github.com/facebook/create-react-app/issues/… Could be related to webpack importing issue. Also at which part did you log the "successfully loaded" message?
-
nulldroid over 2 yearsThank you so much! Your link led me into the right direction. I fixed it now using @svgr/webpack inline loading and excluding inline svg from font file loader in my current project.
-
Kirill over 2 yearsWhere should I look for solution if I'm getting error: "ERROR Error: Cannot find module 'undefined' at webpackMissingModule" ?
-
Denis Molodtsov over 2 yearsWould this work if I get .svg file from a service? For example, we have hundreds of SVG files that we need to render inline. But the time webpack runs, none of these SVG files are present in the soure code. Does anyone know how to approach this?
-
junwen-k over 2 years@DenisMolodtsov Hi, maybe your can look through this question, might be helpful. stackoverflow.com/questions/52964997/…
-
Indigo over 2 yearsIt doesn't work with the latest CRA (react-scripts v5). I have been trying to figure out the changes in the new Webpack v5 config of CRA but can't seem to get it to work so far or fully understand the issue.
Uncaught (in promise) DOMException: Failed to execute 'createElement' on 'Document': The tag name provided ('/static/media/xyz.65482ff5b931f5ffc8ab.svg') is not a valid name.
-
junwen-k over 2 years@Indigo Hi, make sure you are importing
ReactComponent
instead of the default exportimport { ReactComponent as ... } from 'SVG_PATH';
-
Indigo over 2 years@junwen-k the question is about dynamic import
-
junwen-k over 2 years@Indigo Yes, what I meant was when you use import(...), make sure it is taking the ReactComponent like
ImportedIconRef.current = (await import(`./${name}.svg`)).ReactComponent;
It seems like your import statement resolved to asrc
which you usually put into an<img />
tag. I've tested using the latest CRA and seem to have no issue. Care to provide a minimal reproducible sandbox? -
kepes about 2 yearsIf it's not working try to configure webpack with
@svgr
like this: stackoverflow.com/a/70961634/3319170 -
Jordan Soltman about 2 yearsFor anyone still getting
undefined
, there is a bug for dynamic webpack loading when the "issuer" property is set for svgs (github.com/webpack/webpack/discussions/…). The fix is removing the "issurer" config from the svg loader in the webpack config. Since we are using create-react-app, and haven't ejected, we utilizedpatch-package
to remove those lines from the webpack config, and now everything is working. -
b-asaf about 2 years@JordanSoltman: can you share how you remove those line using
patch-package
? @junwen-k: I am getting the same error as @Indigo but I am uingreact-scripts v4.0.3
-
b-asaf about 2 yearsThanks @kraken711 for your insight. When I try your approach I am getting an error:
Module not found: Can't resolve @svgr/webpack?-svgo,+titleProp,+ref!.
I tried : 1.await import(!!@svgr/webpack?-svgo,+titleProp,+ref!./[path_to_icon_folder]/${name}.svg))
2.await import(!!@svgr/webpack?-svgo,+titleProp,+ref!./${name}.svg))
-
Saber Hayati about 2 years@b-asaf You can comment those lines by your self. They're located in
./node_modules/react-scripts/config/webpack.config.js
-
b-asaf almost 2 years@SaberHayati - tried installing
@svgr
based on this comment - stackoverflow.com/questions/55175445/… but with no luck :(