Moshe
Moshe

Reputation: 7007

Getting NextJS Image Component & @svgr/webpack to play nicely together

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?

Upvotes: 10

Views: 11656

Answers (5)

Ivan
Ivan

Reputation: 1

I also working on it and just want to share my solution and touch all nuances. I am using NextJS (new App router) with tailwind CSS. I have next.config.mjs that I grabbed from the answers above.

// next.config.mjs
const nextConfig = {
  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({
      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;
  },
};

export default nextConfig;

I have the following folders structure: src --app --components --assets ----images (png, gif, jpeg ...) ------images.d.ts ----icons (svg) ------svg.d.ts

// svg.d.ts
    declare module "*.svg" {
      const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
      export default content;
    }
    
    /** svg imports with a ?url suffix can be used as the src value in Image components */
    
        declare module "*.svg?url" {
          import type { StaticImport } from "next/image";
        
          const defaultExport: StaticImport | string;
          export default defaultExport;
        }
// images.d.ts
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module '*.webp';

You do not need to import svg.d.ts inside tsconfig.json. And inside my tsx component I can import my logo.svg as React Component or URL (to paste this URL into Next/image)

    import Logo from "assets/icons/logo.svg";
    import logo from "assets/icons/logo.svg?url";

    <Link href="/" className="flex-auto">
                {/* as data url */}
                <Image src={logo} width={155} height={28} alt="logotype" />
              </Link>
              {/* as component */}
              <Logo />

That is all. Good luck.

Upvotes: 0

Chris
Chris

Reputation: 2300

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 />

Upvotes: 3

Loki
Loki

Reputation: 143

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

Upvotes: 7

sudofix
sudofix

Reputation: 63

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

Upvotes: 1

juliomalves
juliomalves

Reputation: 50368

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 />

Upvotes: 11

Related Questions