Dynamic SVG component in Vite + React + TS

Dynamic SVG component in Vite + React + TS

Render dynamic SVG as a component by passing the icon name in vite

ยท

3 min read

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

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

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;

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;

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.

ย