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 (10)

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
 
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
 
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
 
dopamine0 profile image
Roee Fl

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

Collapse
 
njavilas2015 profile image
njavilas2015

great article!

Collapse
 
feralvarz profile image
feralvarz

Thanks for sharing, great article.

Collapse
 
abdullohzenbit profile image
Abdulloh

How it influence bundle size ?

Collapse
 
philmander profile image
Phil Mander

Thanks @mondal10. Very helpful!