🤘

Next.js 15 & Storybook v8.6でSVGRを導入する方法

に公開

SVGをReactコンポーネントとして扱えるSVGRは、UI開発において非常に便利なツールです。本記事では、Next.js 15Storybook v8.6の両方でSVGRを導入し、SVGを快適に扱うための設定方法を解説します。

Next.jsでのSVGR導入

依存パッケージのインストール

pnpm add -D @svgr/webpack

next.config.tsの設定

https://react-svgr.com/docs/next/

SVGR公式ドキュメントに従い、next.config.tsに以下の設定を追加します。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  /* config options here */
  webpack(config) {
    // Grab the existing rule that handles SVG imports
    // @ts-expect-error config.module.rules is not typed
    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: fileLoaderRule.issuer,
        resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
        use: [{ loader: '@svgr/webpack', options: { icon: true } }],
      },
    );

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

    return config;
  },
};

export default nextConfig;

型定義ファイルの追加

TypeScript環境では、SVGインポート用の型定義を追加しましょう。

svgr.d.ts
declare module '*.svg' {
  import type { FC, SVGProps } from 'react';
  const content: FC<SVGProps<SVGElement>>;
  export default content;
}

declare module '*.svg?url' {
  // biome-ignore lint/suspicious/noExplicitAny: any is required here
  const content: any;
  export default content;
}

StorybookでのSVGR導入

StorybookでもSVGをReactコンポーネントとして扱うには、vite-plugin-svgrを利用します。

依存パッケージのインストール

pnpm add -D vite-plugin-svgr

.storybook/main.tsの設定

https://scrapbox.io/ygkn/@storybook%2Fexperimental-nextjs-vite_で_SVGR_を使うには

こちらの記事に従い、.storybook/main.tsに以下の設定を追加します。

.storybook/main.ts
import type { StorybookConfig } from '@storybook/experimental-nextjs-vite';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-onboarding',
    '@chromatic-com/storybook',
    '@storybook/experimental-addon-test',
  ],
  framework: {
    name: '@storybook/experimental-nextjs-vite',
    options: {},
  },
  staticDirs: ['../public'],
  viteFinal(config) {
    config.plugins?.push(tsconfigPaths());
    config.plugins?.push(
      svgr({
        include: /\.svg$/,
      }),
    );
    config.plugins = config.plugins?.flat().map((plugin) => {
      if (
        typeof plugin === 'object' &&
        plugin !== null &&
        'name' in plugin &&
        plugin.name === 'vite-plugin-storybook-nextjs-image'
      ) {
        return {
          ...plugin,
          resolveId(id, importer) {
            if (id.endsWith('.svg')) {
              return null;
            }
            // @ts-expect-error `resolveId` hook of vite-plugin-storybook-nextjs-image is a function
            return plugin.resolveId(id, importer);
          },
        };
      }
      return plugin;
    });
    return config;
  },
};
export default config;
GitHubで編集を提案

Discussion