How to dynamically import SVG and render it inline

36,761

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

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`);
...

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} />
Share:
36,761

Related videos on Youtube

Majoren
Author by

Majoren

Updated on July 09, 2022

Comments

  • Majoren
    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
      wederer about 4 years
      Does your svg display correctly if you statically import?
    • Majoren
      Majoren about 4 years
      Yes! If I do a regular import MyIcon from './icons/myicon.svg' I can render it like <MyIcon />.
    • junwen-k
      junwen-k about 4 years
      You might have to store the resolved SVG in a state instead.
    • Majoren
      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
      junwen-k about 4 years
      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.
    • Majoren
      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 like const Icon = nameToIcon[name]; <Icon />. But it doesn't solve the problem of dynamically importing all the SVGs.
  • Majoren
    Majoren about 4 years
    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 <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 error Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
  • Enchew
    Enchew about 4 years
    Why 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
    Majoren about 4 years
    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
    Majoren almost 4 years
    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
    Majoren almost 4 years
    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": developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
  • junwen-k
    junwen-k almost 4 years
    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
  • Willege
    Willege almost 4 years
    Fantastic use of ref! How would this look in Typescript?
  • junwen-k
    junwen-k almost 4 years
    @Willege I've updated the answer. Feel free to take a look at the code sample.
  • miu
    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
    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
    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
    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
    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
    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
    Nicolás Fantone over 3 years
    Effects can't host an async function. They return a promise which gets invoked as a cleanup function. robinwieruch.de/react-hooks-fetch-data
  • Hoon
    Hoon over 3 years
    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.
  • Mark
    Mark about 3 years
    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?
  • surjit
    surjit about 3 years
    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??
  • junwen-k
    junwen-k about 3 years
    @surjit I think you accidentally wrote triple dots .../ instead of ../.
  • surjit
    surjit about 3 years
    @junwen-k sorry for the typing mistake while commenting.. but in my code I have ../
  • surjit
    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
    junwen-k about 3 years
    @Mark Does restarting your React app helped? Hard to tell with just that error code.
  • surjit
    surjit about 3 years
    @junwen-k I tried restarting the app. No effect 😕
  • surjit
    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
    junwen-k about 3 years
    @surjit I've updated the answer with your current case, hopefully that helps.
  • surjit
    surjit about 3 years
    @junwen-k Yes its working 🙂. giving your answer an up vote.
  • Jasur Kurbanov
    Jasur Kurbanov about 3 years
    Can I use your code for personal and commercial projects ?
  • Augustin Riedinger
    Augustin Riedinger about 3 years
    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?
  • junwen-k
    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
    Augustin Riedinger about 3 years
    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.
  • blueprintchris
    blueprintchris over 2 years
    I get a completely different error: Cannot read property 'dispose' of undefined
  • nulldroid
    nulldroid over 2 years
    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
    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
    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
    nulldroid over 2 years
    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.
  • Kirill
    Kirill over 2 years
    Where should I look for solution if I'm getting error: "ERROR Error: Cannot find module 'undefined' at webpackMissingModule" ?
  • Denis Molodtsov
    Denis Molodtsov over 2 years
    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?
  • junwen-k
    junwen-k over 2 years
    @DenisMolodtsov Hi, maybe your can look through this question, might be helpful. stackoverflow.com/questions/52964997/…
  • Indigo
    Indigo over 2 years
    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.
  • junwen-k
    junwen-k over 2 years
    @Indigo Hi, make sure you are importing ReactComponent instead of the default export import { ReactComponent as ... } from 'SVG_PATH';
  • Indigo
    Indigo over 2 years
    @junwen-k the question is about dynamic import
  • junwen-k
    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 a src 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
    kepes about 2 years
    If it's not working try to configure webpack with @svgr like this: stackoverflow.com/a/70961634/3319170
  • Jordan Soltman
    Jordan Soltman about 2 years
    For 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 utilized patch-package to remove those lines from the webpack config, and now everything is working.
  • b-asaf
    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 uing react-scripts v4.0.3
  • b-asaf
    b-asaf about 2 years
    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))
  • Saber Hayati
    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
    b-asaf almost 2 years
    @SaberHayati - tried installing @svgr based on this comment - stackoverflow.com/questions/55175445/… but with no luck :(