How to dynamically import SVG and render it inline

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

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

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

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

<img src={require(`../assets/${logoNameVariable}`)?.default} />