🤘
Next.js 15 & Storybook v8.6でSVGRを導入する方法
SVGをReactコンポーネントとして扱えるSVGRは、UI開発において非常に便利なツールです。本記事では、Next.js 15とStorybook v8.6の両方でSVGRを導入し、SVGを快適に扱うための設定方法を解説します。
Next.jsでのSVGR導入
依存パッケージのインストール
pnpm add -D @svgr/webpack
next.config.tsの設定
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の設定
こちらの記事に従い、.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;
Discussion