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?
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
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.
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} />;
};
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>
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>
);
"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>
);
};
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
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.
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} />;
};
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;
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,
};
};
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`);