javascriptreactjswebpackwebpack-4

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?


Solution

  • Next.js (SSR)

    If you are using a modern React framework like Next.js that supports SSR, dynamically importing SVG is very easy. The following guide demonstrates the concept, which should be applicable for any other frameworks that support SSR.

    Assuming SVG files are placed in assets folder:

    ├── src
    │   └── assets
    │       ├── a-b-2.svg
    │       └── a-b-off.svg
    
    1. Install @svgr/webpack (or any other SVG loader of your choice), which allow you to import SVG files directly and use them as React components. *Feel free to skip this step if you already have a loader in place.

    2. Add a src/components/lazy-svg.tsx async component.

      import { ComponentProps } from "react";
      
      import dynamic from "next/dynamic";
      
      interface LazySvgProps extends ComponentProps<"svg"> {
        name: string;
      }
      
      export const LazySvg = async ({ name, ...props }: LazySvgProps) => {
        const Svg = dynamic(() => import(`@/assets/${name}.svg`));
      
        // Or without using `dynamic`:
        // We use `default` here because `@svgr/webpack` converts all other *.svg imports to React components, this might be different for other loaders.
        // const Svg = (await import(`@/assets/${name}.svg`)).default;
      
        return <Svg {...props} />;
      };
      
    3. Import and use it as follows:

      import { LazySvg } from "@/components/lazy-svg";
      
      // Server side:
      <LazySvg name="a-b-2" />
      
      // Client side: Wrap with Suspense to render loading state when SVG is loading.
      <Suspense fallback={<>Loading...</>}>
        <LazySvg name="a-b-2" />
      </Suspense>
      

    Usages

    Server Demo

    import { LazySvg } from "@/components/lazy-svg";
    
    export const ServerDemo = () => (
      <div>
        <LazySvg name="a-b-2" stroke="yellow" />
        <LazySvg name="a-b-off" stroke="lightgreen" />
      </div>
    );
    

    Client Demo

    "use client";
    
    import { Suspense, useState } from "react";
    import { LazySvg } from "@/components/lazy-svg";
    
    export const ClientDemo = () => {
      const [name, setName] = useState<"a-b-2" | "a-b-off">("a-b-2");
    
      return (
        <div>
          <button
            onClick={() =>
              setName((prevName) => (prevName === "a-b-2" ? "a-b-off" : "a-b-2"))
            }
          >
            Change Icon
          </button>
          <Suspense fallback={<>Loading...</>}>
            <LazySvg
              name={name}
              stroke={name === "a-b-2" ? "yellow" : "lightgreen"}
            />
          </Suspense>
        </div>
      );
    };
    

    Edit nextjs-dynamic-svg-import

    Vite React (SPA)

    If you are not using SSR, you can dynamically import SVG files using the dynamic import hook. The following guide demonstrates the concept, which should be applicable for any other frameworks that use SPA.

    Assuming SVG files are placed in assets folder:

    ├── src
    │   └── assets
    │       ├── a-b-2.svg
    │       └── a-b-off.svg
    
    1. Install vite-plugin-svgr (or any other SVG loader of your choice), which allow you to import SVG files directly and use them as React components. *Feel free to skip this step if you already have a loader in place.

    2. Add a src/components/lazy-svg.tsx component.

      import { ComponentProps, FC, useEffect, useRef, useState } from "react";
      
      interface LazySvgProps extends ComponentProps<"svg"> {
        name: string;
      }
      
      // This hook can be used to create your own wrapper component.
      const useLazySvgImport = (name: string) => {
        const importRef = useRef<FC<ComponentProps<"svg">>>();
        const [loading, setLoading] = useState(false);
        const [error, setError] = useState<Error>();
      
        useEffect(() => {
          setLoading(true);
          const importIcon = async () => {
            try {
              importRef.current = (
                await import(`../assets/${name}.svg?react`)
              ).default; // We use `?react` here following `vite-plugin-svgr`'s convention.
            } catch (err) {
              setError(err as Error);
            } finally {
              setLoading(false);
            }
          };
          importIcon();
        }, [name]);
      
        return {
          error,
          loading,
          Svg: importRef.current,
        };
      };
      
      // Example wrapper component using the hook.
      export const LazySvg = ({ name, ...props }: LazySvgProps) => {
        const { loading, error, Svg } = useLazySvgImport(name);
      
        if (error) {
          return "An error occurred";
        }
      
        if (loading) {
          return "Loading...";
        }
      
        if (!Svg) {
          return null;
        }
      
        return <Svg {...props} />;
      };
      

    Usage

    import { useState } from "react";
    
    import { LazySvg } from "./components/lazy-svg";
    
    function App() {
      const [name, setName] = useState<"a-b-2" | "a-b-off">("a-b-2");
    
      return (
        <main>
          <button
            onClick={() =>
              setName((prevName) => (prevName === "a-b-2" ? "a-b-off" : "a-b-2"))
            }
          >
            Change Icon
          </button>
          <LazySvg
            name={name}
            stroke={name === "a-b-2" ? "yellow" : "lightgreen"}
          />
        </main>
      );
    }
    
    export default App;
    

    Edit vite-react-ts-dynamic-svg-import

    Create React App (SPA) [Deprecated]

    The majority of Vite's guide applies, with the only difference being on the import method:

    const useLazySvgImport = (name: string) => {
      const importRef = useRef<FC<ComponentProps<"svg">>>();
      const [loading, setLoading] = useState(false);
      const [error, setError] = useState<Error>();
    
      useEffect(() => {
        setLoading(true);
        const importIcon = async () => {
          try {
            importRef.current = (
              await import(`../assets/${name}.svg`)
            ).ReactComponent; // CRA comes pre-configured with SVG loader, see https://create-react-app.dev/docs/adding-images-fonts-and-files/#adding-svgs.
          } catch (err) {
            setError(err as Error);
          } finally {
            setLoading(false);
          }
        };
        importIcon();
      }, [name]);
    
      return {
        error,
        loading,
        Svg: importRef.current,
      };
    };
    

    Caveats

    There’s limitation when using dynamic imports with variable parts. This 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

    <LazySvg path="../../assets/icon.svg" />;
    
    // ...
    
    import(path);
    

    You can change it to

    <LazySvg name="icon" />;
    
    // ...
    
    import(`../../icons/${name}.svg`);