32

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?

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
Majoren
  • 983
  • 5
  • 16
  • 36
  • Does your svg display correctly if you statically import? – wederer Apr 21 '20 at 08:42
  • Yes! If I do a regular `import MyIcon from './icons/myicon.svg'` I can render it like ``. – Majoren Apr 21 '20 at 08:44
  • You might have to store the resolved SVG in a state instead. – junwen-k Apr 21 '20 at 09:08
  • @dev_junwen Correct, but storing it in state still doesn't enable me to render it as an inline svg. – Majoren Apr 21 '20 at 13:42
  • Or 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. – junwen-k Apr 22 '20 at 03:49
  • @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 like `const Icon = nameToIcon[name]; `. But it doesn't solve the problem of dynamically importing all the SVGs. – Majoren Apr 22 '20 at 08:55

6 Answers6

58

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 };
}

Edit react-dynamic-svg-import

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 };
}

Edit react-dynamic-svg-import-ts


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`);
...
junwen-k
  • 3,454
  • 1
  • 15
  • 28
  • 1
    Wow! 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 Apr 30 '20 at 09:49
  • 5
    Btw @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": https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import – Majoren May 14 '20 at 10:41
  • What 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 – junwen-k May 14 '20 at 15:00
  • Fantastic use of `ref`! How would this look in Typescript? –  Jul 06 '20 at 09:19
  • 2
    @Willege I've updated the answer. Feel free to take a look at the code sample. – junwen-k Jul 06 '20 at 17:04
  • 2
    @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)? – miu Jul 09 '20 at 08:49
  • 2
    @mamiu Are you using Create React App ? If not you will need to setup inline svg loader manually. [This](https://stackoverflow.com/a/61713646/11125492) answer might help. – junwen-k Jul 09 '20 at 08:56
  • @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. – miu Jul 09 '20 at 09:59
  • @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. – junwen-k Jul 09 '20 at 12:45
  • 1
    @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 – Salet Jul 17 '20 at 13:06
  • @Salet It appears to be a webpack config bug as described [here](https://github.com/facebook/create-react-app/issues/5276). There are no solutions for now unfortunately. – junwen-k Jul 21 '20 at 06:37
  • I'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? – Mark Feb 23 '21 at 23:39
  • dynamic import not working for me. I get `Cannot find module '.../path/to/file.svg'` . However direct import works `import {ReactComponent as Icon} from '../path/to/file.svg'`. What am I doing wrong?? – surjit Mar 02 '21 at 10:43
  • @surjit I think you accidentally wrote triple dots `.../` instead of `../`. – junwen-k Mar 02 '21 at 13:45
  • @junwen-k sorry for the typing mistake while commenting.. but in my code I have `../` – surjit Mar 02 '21 at 13:47
  • @junwen-k This is the exact error `index.js:1 Error: Cannot find module '../assets/images/sideNav/overview.svg'` – surjit Mar 02 '21 at 13:50
  • @Mark Does restarting your React app helped? Hard to tell with just that error code. – junwen-k Mar 02 '21 at 13:50
  • @junwen-k I tried restarting the app. No effect – surjit Mar 02 '21 at 13:52
  • @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?? – surjit Mar 02 '21 at 13:54
  • @surjit I've updated the answer with your current case, hopefully that helps. – junwen-k Mar 02 '21 at 16:16
  • @junwen-k Yes its working . giving your answer an up vote. – surjit Mar 03 '21 at 15:00
  • Can I use your code for personal and commercial projects ? – Jasur Kurbanov Mar 17 '21 at 05:11
  • I 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? – Augustin Riedinger Apr 13 '21 at 14:05
  • @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. – junwen-k Apr 15 '21 at 03:58
  • Yup, I did this: `let isActive = true; if (isActive) {setLoading(false);} return () => {isActive = false;}` (sorry for ugly formatting). Maybe this is worth updating your answer. – Augustin Riedinger Apr 26 '21 at 10:26
  • I get a completely different error: `Cannot read property 'dispose' of undefined` – blueprintchris Aug 05 '21 at 14:33
  • Please 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 Aug 19 '21 at 14:07
  • @junwen-k directly downloaded your codesandbox files. Same issue. npm --version 7.20.6 , node --version v14.16.0. I don't get it. – nulldroid Aug 19 '21 at 14:35
  • 1
    @Nulldroid Does following this solution helps? https://github.com/facebook/create-react-app/issues/5276#issuecomment-665628393 Could be related to webpack importing issue. Also at which part did you log the "successfully loaded" message? – junwen-k Aug 20 '21 at 01:52
  • Thank 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. – nulldroid Aug 20 '21 at 14:33
  • Where should I look for solution if I'm getting error: "ERROR Error: Cannot find module 'undefined' at webpackMissingModule" ? – Kirill Oct 10 '21 at 08:08
  • Would 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? – Denis Molodtsov Oct 22 '21 at 22:31
  • @DenisMolodtsov Hi, maybe your can look through this question, might be helpful. https://stackoverflow.com/questions/52964997/react-import-svg-inline-from-url – junwen-k Oct 23 '21 at 03:36
  • 1
    It 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.` – Indigo Dec 26 '21 at 13:11
  • @Indigo Hi, make sure you are importing `ReactComponent` instead of the default export ```import { ReactComponent as ... } from 'SVG_PATH';``` – junwen-k Jan 03 '22 at 14:17
  • @junwen-k the question is about dynamic import – Indigo Jan 03 '22 at 17:52
  • @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 a `src` which you usually put into an `` tag. I've tested using the latest CRA and seem to have no issue. Care to provide a minimal reproducible sandbox? – junwen-k Jan 04 '22 at 04:53
  • If it's not working try to configure webpack with `@svgr` like this: https://stackoverflow.com/a/70961634/3319170 – kepes Feb 02 '22 at 19:56
  • 3
    For anyone still getting `undefined`, there is a bug for dynamic webpack loading when the "issuer" property is set for svgs (https://github.com/webpack/webpack/discussions/15117#discussioncomment-1925385). 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 utilized `patch-package` to remove those lines from the webpack config, and now everything is working. – Jordan Soltman Feb 13 '22 at 06:09
  • @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 uing `react-scripts v4.0.3` – b-asaf Mar 20 '22 at 14:05
  • @b-asaf You can comment those lines by your self. They're located in `./node_modules/react-scripts/config/webpack.config.js` – Saber Hayati Apr 13 '22 at 13:21
  • @SaberHayati - tried installing `@svgr` based on this comment - https://stackoverflow.com/questions/55175445/cant-import-svg-into-next-js/70961634#70961634 but with no luck :( – b-asaf May 02 '22 at 14:05
  • I keep getting the error: Failed to execute 'createElement' on 'Document': The tag name provided ('/static/media/icons8-team.7d336a6dcad1b0b4df3e.svg') is not a valid name. I tried using "ReactComponent" as the import, but then I am getting undefined and the icon is not showing. So it seems I have to use the "default" any idea how to fix this? – Yousi Sep 29 '22 at 17:13
  • @Yousi By any chance that you are using Vite? You may take a look at https://stackoverflow.com/a/70685179/11125492 and see if it helps. – junwen-k Oct 01 '22 at 03:40
  • Thanks @junwen-k for your reply. But I am not use Vite. But I noticed something, when I type the path of the icon manually (without using the template literal) it works fine and I can see the SVG icon. It seems that webpack doesn't like variables – Yousi Oct 01 '22 at 04:38
15

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 }/>;
};
Enchew
  • 981
  • 5
  • 11
  • 4
    This 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 . 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 , and it gives me the error `Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.` – Majoren Apr 21 '20 at 12:30
  • Why do you need to render it as an `` ? What does this approach give you and is there any difference, that can't be overcome by using `` – Enchew Apr 21 '20 at 12:48
  • 2
    How 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 Apr 21 '20 at 13:38
  • Effects can't host an `async` function. They return a promise which gets invoked as a cleanup function. https://www.robinwieruch.de/react-hooks-fetch-data – Nicolás Fantone Aug 31 '20 at 10:14
  • Regardless 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. – Hoon Jan 05 '21 at 22:45
  • Not working for me, I'm using craco, how to override webpack configuration ? – Tania12 May 16 '23 at 12:09
5

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;
    };
kraken711
  • 51
  • 1
  • 3
  • Thanks @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))` – b-asaf Mar 24 '22 at 09:44
2

One solution to load the svg dynamically could be to load it inside an img using require, example:

<img src={require(`../assets/${logoNameVariable}`)?.default} />
Juanma Menendez
  • 17,253
  • 7
  • 59
  • 56
1

i changed my code to this and work:

import { ReactComponent as Dog } from './Dog.svg';

use like this:

<Dog />

or if it is dynamic:

 import * as icons from '../../assets/categoryIcons';

 const IconComponent = icons[componentName??'Dog'];

 <IconComponent fill='red' />
lcyper
  • 45
  • 4
-1

I dynamically fetched the SVG file as text and then put the SVG within a div dangerouslySetInnerHTML.

  const Icon = ({ className, name, size = 16 }: IconProps) => {
  const [Icon, setIcon] = React.useState("");

  React.useEffect(() => {
    fetch(`/icons/${name}.svg`)
      .then((res) => res.text())
      .then((res) => {
        if (res.startsWith("<svg")) return setIcon(res);
        console.error(
          `Icon: "${name}.svg" not found in ${process.env.PUBLIC_URL}/icons`
        );
        return setIcon("");
      });
  }, [name]);

  if (!Icon) return null;

  return (
    <div
      className={classNames("icon", className)}
      style={{ width: !size ? "100%" : size + "px", height: "100%" }}
      dangerouslySetInnerHTML={{ __html: Icon }}
    />
  );
};

Preview on Codesandbox

You can automatically change the color of your svg by giving it a fill value of "currentColor".

AbrahamDN
  • 39
  • 3