Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dynamically import SVG and render it inline

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?

like image 265
Majoren Avatar asked Apr 21 '20 08:04

Majoren


People also ask

How import SVG dynamically react?

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

Is SVG dynamic?

You can see that the final markup for both SVG is exactly the same. The only difference is one is static in the HTML, the other is created dynamically via javascript.

Can you respond to render SVG?

There are a few ways to use an SVG in a React app: Use it as a regular image. Import it as a component via bundler magic (SVGR) Include it directly as JSX.

Is there a way to dynamically inline SVGS?

Problem There a 3rd party libraries that can help to dynamically inline SVGs. I have worked with one of them (svg-sprite) on a previous project. It's nice as in terms of syntax you have something like this when you need to inline an SVG: Basically, you pass the name of the icon that you want to render. Pretty easy right?

Why is my React component getting undefined when importing SVG files?

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.

Does your SVG display correctly if you statically import?

Does your svg display correctly if you statically import? Yes! If I do a regular import MyIcon from './icons/myicon.svg' I can render it like <MyIcon />.

What is the fastest way to load an SVG file?

Because inline SVG is embedded into HTML, there is no necessity for another network request to obtain the SVG file, and therefore inline SVG will load the fastest. Be aware that you will still get FOUC (Flash of unstyled content) because chances are, your inline SVG will still refer to an external font.


4 Answers

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`);
...
like image 117
junwen-k Avatar answered Oct 23 '22 10:10

junwen-k


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 }/>;
};
like image 38
Enchew Avatar answered Oct 23 '22 11:10

Enchew


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;
    };
like image 5
kraken711 Avatar answered Oct 23 '22 12:10

kraken711


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

<img src={require(`../assets/${logoNameVariable}`)?.default} />
like image 2
Juanma Menendez Avatar answered Oct 23 '22 11:10

Juanma Menendez