DEV Community

Cover image for Dynamic SVG component in Vite + React + TS
Amit Mondal
Amit Mondal

Posted on

Dynamic SVG component in Vite + React + TS

After hearing many amazing things about vite I decided to try it and create something and I am not at all disappointed with the DX. So when I was developing this fun project of mine (which is currently WIP) I thought of creating a dynamic SVG component that would load up just by giving the SVG icon name with all the SVG customizable attributes under my control.

The first hurdle was to find a plugin that can transform SVG to React component just like we have in CRA. After a few google searches landed on vite-plugin-svgr.

Install vite-plugin-svgr:

If you are using CRA for your React project then this step can be skipped as it configures SVGR(SVGR lets you transform SVGs into React components) under the hood for you.

To install the plugin run:

yarn add -D vite-plugin-svgr

Setting up vite-plugin-svgr:

Register the installed plugin in the vite config file, easy peasy!

File: vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr"; // transform svg to react component

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), svgr()], // register the plugin
});
Enter fullscreen mode Exit fullscreen mode

Writing the custom hook:

Once SVGR setup is done we are ready to create the functionality that will grab the icon-name and fetch it from assets and load the same. We can also modify the hook so that it can take the icon's path and load it. From this hook, we export the loading state, error state, and the component itself.

File: useDynamicSvgImport.ts

import React, { useEffect, useRef, useState } from "react";

export function useDynamicSvgImport(iconName: string) {
  const importedIconRef = useRef<React.FC<React.SVGProps<SVGElement>>>();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<unknown>();

  useEffect(() => {
    setLoading(true);
    // dynamically import the mentioned svg icon name in props
    const importSvgIcon = async (): Promise<void> => {
      // please make sure all your svg icons are placed in the same directory
      // if we want that part to be configurable then instead of iconName we will send iconPath as prop
      try {
        importedIconRef.current = (
          await import(`../../assets/icons/${iconName}.svg`)
        ).ReactComponent; // svgr provides ReactComponent for given svg path
      } catch (err) {
        setError(err);
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    importSvgIcon();
  }, [iconName]);

  return { error, loading, SvgIcon: importedIconRef.current };
}
Enter fullscreen mode Exit fullscreen mode

Writing the component:

Let's write the wrapper component for our SVG, main aim of this wrapper is to provide the loading shimmer when the icon is not present to be displayed and apply some wrapper style and SVG attributes.

File: SvgIcon.tsx

import { useDynamicSvgImport } from "./useDynamicSvgImport";

interface IProps {
  iconName: string;
  wrapperStyle?: string;
  svgProp?: React.SVGProps<SVGSVGElement>;
}

function SvgIcon(props: IProps) {
  const { iconName, wrapperStyle, svgProp } = props;
  const { loading, SvgIcon } = useDynamicSvgImport(iconName);

  return (
    <>
      {loading && (
        <div className="rounded-full bg-slate-400 animate-pulse h-8 w-8"></div>
      )}
      {SvgIcon && (
        <div className={wrapperStyle}>
          <SvgIcon {...svgProp} />
        </div>
      )}
    </>
  );
}

export default SvgIcon;
Enter fullscreen mode Exit fullscreen mode

Using component:

Now the component is ready to be used with all the SVG element attributes and wrapper styles that can be passed on as a prop.

File: App.tsx

import './App.css';
import SvgIcon from './SvgIcon';

function App() {
  return (
    <div>
      <h1>Hello!</h1>
      <SvgIcon
        iconName="react"
        svgProp={{ width: 100, height: 100, fill: "#61dafb" }}
      />
      <SvgIcon iconName="react" svgProp={{ stroke: "white", fill: "blue" }} />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Working example link:

Do let me know if there are any better ways in the comments below.
Thanks for your time reading. Drop a few ❤️ or 🦄 if you liked it.

Top comments (12)

Collapse
 
hazemalabiad profile image
Hazem Alabiad •

Amazing!
I really appreciate you :)

I have a question why this solution does not work when we use useState instead of useRef?

Collapse
 
mondal10 profile image
Amit Mondal • • Edited

Sorry for late comment.

A small note for anyone who tries useState instead of useRef: React treats a function passed to a setter as an updater, so setSvgIcon(ReactComponent) will call the component instead of storing it. Wrap it in an arrow function to fix it:

setSvgIcon(() => ReactComponent);
Enter fullscreen mode Exit fullscreen mode

useRef skips this because assigning to ref.current is a plain assignment.

Also, if you’re on vite-plugin-svgr v4+, import with ?react and use the default export instead of ReactComponent.

Collapse
 
jradams1997 profile image
Justin Adams •

The author isn't going to answer questions because he doesn't know. He just stole the code from: StackOverflow without crediting the author. Shameful.

Collapse
 
njavilas2015 profile image
njavilas2015 •

great article!

Collapse
 
feralvarz profile image
feralvarz •

Thanks for sharing, great article.

Collapse
 
dopamine0 profile image
Roee Fl •

Thank you @mondal10, so helpful, you really rescued me in despair.

Collapse
 
philmander profile image
Phil Mander •

Thanks @mondal10. Very helpful!

Collapse
 
dopamine0 profile image
Roee Fl •

Lmao @jradams1997 pwned

Collapse
 
jradams1997 profile image
Justin Adams •

Did I say anywhere in my comment that it was my code? Also, I know it’s probably too hard for you, but you can press the ‘Reply’ button to directly reply. Have a nice day.

Collapse
 
dopamine0 profile image
Roee Fl •

Toxic mate

Collapse
 
abdullohzenbit profile image
Abdulloh •

How it influence bundle size ?

Collapse
 
mondal10 profile image
Amit Mondal •

It’s a mixed bag, but mostly good.

Pros:

  • Vite (Rollup) sees import(../../assets/icons/${iconName}.svg) and splits every SVG in that folder into its own chunk. None of them land in the main bundle, so the initial load stays small.
  • An icon is only downloaded when it’s first rendered, then cached.

Cons:

  • Since the name is dynamic, Vite can’t tell which icons you use, so all icons in that folder get built as chunks, even unused ones. Build output grows, but users only download what they render.
  • Each icon is a separate request, so a page with many different icons means many small requests (and a brief shimmer on first load).
  • Each chunk is a React component (JS), so it’s slightly larger than the raw .svg.

Tip: for icons shown on every page (logo, nav), import them statically so they ship in the main bundle and don’t flash a loader. Keep the dynamic folder limited to icons that really are on-demand.