9

I have a Next.js site with the @svgr/webpack library installed. I have configured next.config.js to work with @svgr/webpack and now want to import an svg image and use it with the new next/image component.

Here is how I set up my next.config.js file:

module.exports = {
  images: {
    domains: ["images.vexels.com"],
  },
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack"],
    });

    return config;
  },
};

And here is what I am trying to do:

import Image from 'next/image'
import Logo from '@/svg/logo.svg'

<Image src={Logo} width={174} height={84} />

However, when I do that I get the following error:

Unhandled Runtime Error
TypeError: src.startsWith is not a function

Source
client\image.tsx (278:13) @ Image

  276 | let isLazy =
  277 |   !priority && (loading === 'lazy' || typeof loading === 'undefined')
> 278 | if (src && src.startsWith('data:')) {
      |           ^
  279 |   // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
  280 |   unoptimized = true
  281 |   isLazy = false

I thought that perhaps I should include the Logo component as an actual component, like this: <Image src={<Logo />} width={174} height={84} />

However, that also did not work.

Any idea what is wrong and how to fix it?

juliomalves
  • 42,130
  • 20
  • 150
  • 146
Moshe
  • 6,011
  • 16
  • 60
  • 112

4 Answers4

8

With your current webpack config importing @/svg/logo.svg will only import the SVG file as a React component.

To import it as a data URL, you will need the following webpack config in your next.config.js.

module.exports = {
    images: {
        domains: ['images.vexels.com']
    },
    webpack(config) {
        config.module.rules.push({
            test: /\.svg$/,
            use: ['@svgr/webpack', 'url-loader']
        });

        return config;
    }
};

You'll then be able to use it in both ways: as a URL or as a component.

import Image from 'next/image'
import svgUrl, { ReactComponent as Logo } from '@/svg/logo.svg'

<Image src={svgUrl} width={174} height={84} />
// or as a component
<Logo />
juliomalves
  • 42,130
  • 20
  • 150
  • 146
  • Thanks. I am wondering, are there any downsides to importing it as a data URL? – Moshe Mar 23 '21 at 18:17
  • If you really want to use `next/image` there's not much of a choice, you have to use a URL. – juliomalves Mar 23 '21 at 18:28
  • In some cases I have the option of putting the svg file in the public folder and calling it regular -- i.e., `src="/img/logo.svg`. That is not always the case. But when it is the case, I am wondering are there any downsides to using a data URL? – Moshe Mar 24 '21 at 12:00
  • Just that it seems unnecessary if you can just reference the URL to the SVG file. Keep in mind that if you do load the SVG via `@svgr/webpack` it will be part of the resulting webpack bundle, while referencing it directly from the public folder via URL won't. – juliomalves Mar 24 '21 at 14:37
2

Other answers sacrifice the default width + height importing behavior provided by NextJS. My answer below retains this behaviour so that you don't need to manually check the dimensions of the file

Desired usage

import MySVG from "./mySVG.svg?svgr"; // SVGR loader

<MySVG />

import Image from "next/image";
import mySVG from "./mySVG.svg"; // Default NextJS loader

<Image src={mySVG} alt="" /> // (width and height will be applied automatically)

Required next.config.js

webpack(config, { dev: isDev, isServer }) {

    config.module.rules.push({
      test: /\.svg$/i,
      issuer: /\.[jt]sx?$/,
      resourceQuery: /svgr/, // only use svgr to load svg if path ends with *.svg?svgr
      use: ["@svgr/webpack"],
    });

    // Re-add default nextjs loader for svg
    config.module.rules.push({
      test: /\.svg$/i,
      loader: "next-image-loader",
      issuer: { not: /\.(css|scss|sass)$/ },
      dependency: { not: ["url"] },
      resourceQuery: { not: [/svgr/] }, // Ignore this rule if the path ends with *.svg?svgr
      options: { isServer, isDev, basePath: "", assetPrefix: "" },
    });

}

Required typescript declaration (if using ts)

declare module "*.svg?svgr";

How I figured it out

  1. Read these docs: https://react-svgr.com/docs/webpack/
  2. Used this snippet to get the default rule applying to svgs
webpack(config) {
    const defaultSvgLoader = config.module.rules.find(
      (rule) => typeof rule?.test?.test === "function" && rule.test.test(".svg")
    );
    console.log(defaultSvgLoader);
}
  1. Added resourceQuery: { not: [/svgr/] } into the logged output object so that *.svg?svgr paths will be ignored
Loki
  • 53
  • 5
1

A workaround for this maybe by having a specific pattern for svg file name, and then configuring the default loader to ignore this pattern and svgr/webpack to load matches for this pattern

 webpack(config) {
    const fileLoaderRule = config.module.rules.find(
      (rule) => rule.test && rule.test.test(".svg")
    );
    fileLoaderRule.exclude = /\.icon\.svg$/;
    config.module.rules.push({
      test: /\.icon\.svg$/,
      loader: require.resolve("@svgr/webpack"),
    });
    return config;
  },

here i'm using the pattern *.icon.svg, so any svg image that ends with it can be used like this

import Logo from "whatever/logo.icon.svg

const Whatever = () => <Logo />

and for other icons this'll work

import Image from "next/image";
import Logo from "whatever/logo.svg"

const Whatever = () => <Image src={Logo} alt="logo" width={100} height={100}/>
sudofix
  • 63
  • 2
  • 5
1

This is similar to previous answers, but the only way that worked for me to allow for both url-only imports as well as React component imports:

// next.config.js

module.exports = {
  webpack(config) {
    // Grab the existing rule that handles SVG imports
    const fileLoaderRule = config.module.rules.find(
      (rule) => rule.test && rule.test.test?.(".svg")
    );

    config.module.rules.push({
      oneOf: [
        // 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;
  },

  // ...other config
};

TypeScript declaration (if needed)

// svg.d.ts

/** svg imports with a ?url suffix can be used as the src value in Image components */
declare module "*.svg?url" {
  import { StaticImport } from "next/image";

  const defaultExport: StaticImport | string;
  export default defaultExport;
}

Sample usage

import Image from "next/image";
import Icon from "./my-icon.svg";
import iconUrl from "./my-icon.svg?url"

// ...

<Image src={iconUrl} />
<Icon />
Chris
  • 2,174
  • 28
  • 37