kostumブログ

勉強したことやノート代わりのアウトプットに使っています。

Next.js + Storybook(Webpack5) + TypeScriptでsvgファイルを表示する

環境

技術 バージョン
React.js ^18
Next.js 14.0.3
Storybook ^7.6.6
svgr ^8.1.0
Webpack @ Storybook 5

問題

Storybookを起動すると、ビルドは成功するが、Failed to execute 'createElement' on 'Document': The tag name provided ('static/media/public/images/icons/check.svg') is not a valid name.のエラーが出てしまう。

前提

アイコンを表示するコンポーネントを作成しています。

// Iconコンポーネント
import Check from '/public/images/icons/check.svg';

const ICONS = { Check };

type IconName = keyof typeof ICONS;
type Size = 16 | 24 | 32 | 64;
type Props = {
    name: IconName;
    size: Size;
};

export default function Icon({ name, size }: Props) {
    const Icon = ICONS[name];
    return <Icon height={size} width={size}></Icon>;
}

イコン画像は、Next.jsプロジェクトのルートディレクトリのpublicディレクトリ内に作成しています。

Next.js側の設定では、Webpackにsvgrの設定を行っています。 また、コンポーネント内で画像をimportしたときの型定義設定を無効にしています。 (そのため、svgファイルをimportした場合の独自の型定義を作成していますが、ここでは割愛させていただきます)

/** @type {import('next').NextConfig} */
const nextConfig = {
    webpack: (config) => {
        config.module.rules.push({
            test: /\.svg$/,
            use: [
                {
                    loader: '@svgr/webpack',
                    options: {},
                },
            ],
        });
        return config;
    },
    images: {
        disableStaticImages: true,
    },
};

export default nextConfig;

Storybookの設定は以下になります。

// .storybook/main.ts

import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';  

const config: StorybookConfig = {
    stories: ['../src/**/*.stories.(tsx)'],
    addons: [
        '@storybook/addon-links',
        '@storybook/addon-essentials',
        '@storybook/addon-onboarding',
        '@storybook/addon-interactions',
    ],
    framework: {
        name: '@storybook/nextjs',
        options: {},
    },
    staticDirs: ['../public'],
    docs: {
        autodocs: 'tag',
    },
    webpackFinal: async (config) => {
        if (config.resolve) {
            config.resolve.alias = {
                ...config.resolve.alias,
                '~': [path.resolve(__dirname, '../src/')],
            };
        }

        if (config.module) {        
            config.module.rules = [
                ...(config.module.rules || []),
                {
                    test: /\.svg$/i,
                    issuer: /\.tsx?$/,
                    use: [
                        {
                            loader: '@svgr/webpack',
                            options: {},
                        },
                    ],
                },
            ];
        }

        return config;
    },
};

export default config;

原因

github.com

公式において、上記のコードでsvg用のloaderを設定しています。

// code/builders/builder-webpack5/src/preview/base-webpack.config.ts

// (省略...)

{
  test: /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
  type: 'asset/resource',
  generator: {
    filename: isProd
      ? 'static/media/[name].[contenthash:8][ext]'
      : 'static/media/[path][name][ext]',
  },
},

// (省略...)

これによると、Webpack5のAsset Modulesである asset/resourceを使っています。

webpack.js.org

こちらの読み込みを優先してしまい、パスが正しい読み込みをできておらず、エラーが出ているとのことでした。

解決

上記のAsset Modulesを使った読み込みを外せば良さそうです。

ということで、svgファイルを読み込むloaderを持つルールに、svgを除外する設定を行います。 (変更部分だけ記載します)

// .storybook/main.ts

// (省略...)

const config: StorybookConfig = {
    
    // (省略...)
    
    webpackFinal: async (config) => {
    
        // (省略...)

        if (config.module) {
            const newRule = config.module.rules?.map((rule) => {
                if (
                    rule &&
                    typeof rule === 'object' &&
                    rule.test?.toString().includes('svg')
                ) {
                    return { ...rule, exclude: /\.svg$/ };
                }
                
                return rule;
            });
            
            config.module.rules = [
                ...(newRule || []),
                {
                    test: /\.svg$/i,
                    issuer: /\.tsx?$/,
                    use: [
                        {
                            loader: '@svgr/webpack',
                            options: {},
                        },
                    ],
                },
            ];
        }

        // (省略...)

    },
};

// (省略...)

上記設定を行うことで、無事Storybookでアイコンコンポーネントが表示できました。

参考

こちらの記事を参考にさせていただきました。

zenn.dev