gregberge / svgr

Transform SVGs into React components 🦁
https://react-svgr.com
MIT License
10.59k stars 422 forks source link

SVGR dynamic import with Webpack (& React Lazy?) #724

Open jameswilson opened 2 years ago

jameswilson commented 2 years ago

💬 Questions and Help

Thank you for this helpful project!

I'm trying to dynamically import SVGs based on a string filename, but I'm a little lost.

Is something like this possible using webpack import?

import React, { Suspense } from "react";

const Logo = ({ brand }) => {
  const Svg = React.lazy(() => import(`../logos/${brand}.svg`));

  return (
    <Suspense fallback={<>{/* loader */}</>}>
      <Svg className={brand} />
    </Suspense>
  );
};

When I run the webpack build, I get same error for every SVG in the folder.

ERROR in ./src/logos/somebrand.svg 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type,
currently no loaders are configured to process this file. 
See https://webpack.js.org/concepts#loaders

webpack.config.js setup following docs:

  return {
    module: {
      rules: [    
        {
          test: /\.svg$/i,
          issuer: /\.[jt]sx?$/,
          use: ['@svgr/webpack'],
        },
      ],
    },
  };

I've tried adding 'file-loader', to the use to no avail. Apologies if I'm off track in some obvious way, I'm fairly new to SVGR & React. I read somewhere that SVGR creates a mapping during build, so if there is a simpler way to just load the right SVG dynamically based on a string, an example would be great to have in docs. My apologies if this is documented already, I searched, scanned, and couldn't find anything relevant.

julianklumpers commented 2 years ago

Same issue with me

gregberge commented 2 years ago

Hi, I am very surprised that it does not work. I think it is a webpack configuration issue more than a SVGR one. By looking at your example I have no idea why it does not work. Maybe someone else could guess.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

huazhuangnan commented 1 year ago

@jameswilson @julianklumpers webpack.config.js remove issuer: /\.[jt]sx?$/,

Matt-Tranzact commented 1 year ago

How about for rollup, is there any way to load svg dynamically similar to webpack?

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

johnschult commented 1 year ago

@jameswilson @julianklumpers webpack.config.js remove issuer: /\.[jt]sx?$/,

If anyone is finding this issue because they used the SVGR NextJS docs to setup webpack, removing the issuer line fixes the error below:

Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type,
currently no loaders are configured to process this file. 

FWIW I am using Next 13 with app router and until I removed that line it failed.

nachten commented 1 year ago

I'm facing a different problem, hopefully someone can help me. I have an UI library with an Icon component:

import clsx from 'clsx';
import styles from './Icon.module.scss';
import { icons } from './icons';
import { Suspense, useMemo } from 'react';

export type IconName = keyof typeof icons;

export type IconProps = React.HTMLAttributes<HTMLDivElement> & {
    icon: IconName;
    disabled?: boolean;
};

export const Icon: React.FC<IconProps> = ({ icon, disabled = false, ...props }: IconProps) => {
    const SvgIcon = useMemo(() => icons[icon], [icon]);
    const classNames = clsx(styles.root, disabled ? styles.disabled : '', props.className);

    if (!SvgIcon) return null;

    return (
        <div
            {...props}
            className={classNames}
        >
            <Suspense fallback={null}>
                <SvgIcon style={{ width: '100%', height: '100%' }} />
            </Suspense>
        </div>
    );
};

export default Icon;

The content of my icons.ts looks like this

import React from 'react';

const lazy = (componentImportFn: Function) =>
    React.lazy(async () => {
        let obj = await componentImportFn();
        return typeof obj.default === 'function' ? obj : obj.default;
    });

export const icons = {
    Icon1: lazy(async () => import('./assets/ico-icon1.svg')),
    Icon2: lazy(async () => import('./assets/ico-icon2.svg')),
    Icon3: lazy(async () => import('./assets/ico-icon3.svg')),
    Icon4: lazy(async () => import('./assets/ico-icon4.svg')),
}

And my NextJs config looks like this as described here

/** @type {import('next').NextConfig} */

const nextConfig = {
    output: 'export',
    distDir: 'build-next-static',
    swcMinify: true,
    reactStrictMode: true,
    webpack(config) {
        // Grab the existing rule that handles SVG imports
        const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg'));

        config.module.rules.push(
            // Reapply the existing rule, but only for svg imports ending in ?url
            {
                ...fileLoaderRule,
                test: /\.svg$/i,
                resourceQuery: /url/, // *.svg?url
            },
            // Convert all other *.svg imports to React components
            {
                test: /\.svg$/i,
                // issuer: /\.[jt]sx?$/,
                resourceQuery: { not: /url/ }, // exclude if *.svg?url
                use: ['@svgr/webpack'],
            }
        );

        // Modify the file loader rule to ignore *.svg, since we have it handled now.
        fileLoaderRule.exclude = /\.svg$/i;

        return config;
    },
};

module.exports = nextConfig;

And when calling the Icon button in a nextjs page e.g.

import { Icon } from 'ui';

export default function Page() {
    return (
        <>
            <Icon icon="Icon1" />
        </>
    );
}

I'm getting the following error from nextjs

Unhandled Runtime Error Error: Unsupported Server Component type: undefined

Can someone push me in a direction or better have a solution for me ?

epavletic commented 12 months ago

I have a similar problem, although with a slightly different error:

So, usage looks something like this (we’re using @svgr/webpack to transform svg’s on the fly):

const logos = {
  brand1: lazy(() => import('path/to/brand1logo.svg')),
  brand2: lazy(() => import('path/to/brand2logo.svg')),
};

const LogoComponent = ({ brand }) => {
  const Logo = logos[brand];

  return (
    <Suspense fallback={null}>
      <Logo />
    </Suspense>
  )
}

…and what I’m getting in the console is the following:

react-dom.development.js:17733 Uncaught Error: Element type is invalid. Received a promise that resolves to: /path/to/logo.svg. Lazy element type must resolve to a class or function.

It feels like we’re getting back something that isn’t a valid component, so React does not know what to do with it. In my head, I’d figure that SVGR would do the transformation from svg → react component before the promise resolves (or, at least before React sinks its teeth in it), but perhaps that’s not the case? Any ideas?

epavletic commented 12 months ago

Relevant addendum: We’re still on webpack 4.x.x, which I’m beginning to suspect might be part of the problem…

In another greenfield project where we’re using Next, we’re able to do the above just fine (just using Next’s dynamic() instead of lazy/Suspense). We had to move away from using variables as part of the dynamic import path though (../logos/${brand}.svg) – never got that to work, regardless of which setup we introduced it to.