🫥

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

2024/07/28に公開

環境

技術 バージョン
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;

原因

https://github.com/storybookjs/storybook/blob/next/code/builders/builder-webpack5/src/preview/base-webpack.config.ts#L54-L62

公式において、上記のコードで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を使っています。

https://webpack.js.org/guides/asset-modules/

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

解決

上記の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でアイコンコンポーネントが表示できました。

参考

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

https://zenn.dev/nbstsh/scraps/c91b268ca517a9

転載元

https://kostum.hatenablog.jp/entry/2023/12/30/024702

Discussion