[@svgr/webpack] Next.js / Storybook でSVGをコンポーネントとして扱う
やりたいこと
- リポジトリにSVGファイルを格納して、Reactコンポーネントから扱いたい
- Next.jsとStorybook両方で表示したい
記事要約
- @svgr/webpackを利用し、webpackの設定を書いて解決する
- Next.jsで表示するのは簡単だが、Storybookでは多くの罠があった
前提情報
- Next.js製の筆者の個人ブログで実装しています: https://github.com/MH4GF/log.mh4gf.dev
- この記事の前身となるスクラップに作業ログを記載しています: https://zenn.dev/mh4gf/scraps/57b84180a70ae7
- この記事での最終的な成果物・コミットログはPull Requestにまとまっています: https://github.com/MH4GF/log.mh4gf.dev/pull/70
Next.jsでの表示
まず@svgr/webpackを追加します。
yarn add -D @svgr/webpack
続いて next.config.js にwebpackの設定を加筆します。
// next.config.js
module.exports = {
  ~~~ 省略 ~~~
  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })
    return config
  },
}
SVGファイルを格納し、適当なコンポーネントでimportし、JSXに加筆します。
import ExternalLinkIcon from '~/src/assets/svg/external-link.svg'
const App = () => (
  <div>
    <ExternalLinkIcon />
  </div>
)
これだけでNext.jsでSVGの表示ができるようになります。
Storybookでの表示
./storybook/main.js にもwebpackの設定を加筆します。
// ./storybook/main.js
  webpackFinal: async (config) => {
    ~~ 省略 ~~
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })
    return config
  },
この状態でStorybookのビルドを実行すると、 TypeError: this.getOptions is not a function が発生します。
ERR! => Failed to build the preview
ERR! ./src/assets/svg/external-link.svg
ERR! Module build failed (from ./node_modules/@svgr/webpack/dist/index.js):
ERR! TypeError: this.getOptions is not a function
ERR!     at Object.svgrLoader (/home/runner/work/log.mh4gf.dev/log.mh4gf.dev/node_modules/@svgr/webpack/dist/index.js:83:24)
ERR!  @ ./src/components/ArticleListItem/index.tsx 2:0-66 24:33-49
ERR!  @ ./src/components/ArticleListItem/index.stories.tsx
ERR!  @ ./src sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$
ERR!  @ ./generated-stories-entry.js
 TypeError: this.getOptions is not a function
この問題はWebpack5を使うように修正すれば解決するとのことでした。ref:
StorybookでWebpack5を利用するためのドキュメントに従い、アップグレードします。
yarn add --dev @storybook/builder-webpack5 @storybook/manager-webpack5
// ./storybook/main.js
module.exports = {
  core: {
    builder: 'webpack5',
  },
  ~~~ 省略 ~~~
これで this.getOptions のエラーは起きなくなりましたが、以下のエラーが発生します。
$ yarn storybook
yarn run v1.22.11
$ start-storybook -p 6006
info @storybook/react v6.4.22
info 
info => Loading presets
info => Serving static files from ././public at /
info => Using PostCSS preset with postcss@8.4.13
info => Using default Webpack5 setup
<i> [webpack-dev-middleware] wait until bundle finished
9% setup compilation DocGenPlugininternal/modules/cjs/loader.js:905
  throw err;
  ^
Error: Cannot find module 'webpack/lib/util/makeSerializable.js'
 Cannot find module 'webpack/lib/util/makeSerializable.js'
これは以下のissueのワークアラウンドにて解決できました。 ref:
./storybook/main.jsを以下のように修正します。
// ./storybook/main.js
module.exports = {
  ~~~ 省略 ~~~
  typescript: { reactDocgen: false },
}
StorybookでのWebpack5対応はまだexperimentalなので仕方なさそうか…と考えています。
これによりビルドは通るようになりましたが、SVGがimportされているコンポーネントのstoryの表示でエラーが起きています。
Failed to execute 'createElement' on 'Document': The tag name provided ('static/media/external-link.981294d1.svg') is not a valid name.
 Failed to execute 'createElement' on 'Document': The tag name provided ('static/media/external-link.981294d1.svg') is not a valid name.
このエラーは、Storybookがデフォルトで用意するwebpackの設定でsvgの拡張子がfile-loaderでロードされるようになっているのが原因でした。
それを打ち消すように設定すれば解決しました。
  webpackFinal: async (config) => {
    ~~ 省略 ~~
    const fileLoaderRule = config.module.rules.find((rule) => rule.test && rule.test.test('.svg'))
    fileLoaderRule.exclude = /\.svg$/
    return config
  },
上記のいくつかの対応によってStorybookでもSVGの表示ができるようになりました。
終わりに
ワークアラウンド的な方法で解決してしまったこともあり、今後の関連ライブラリのアップデートを追おうと思っています。この記事へのPRも大歓迎です。
今回は @svgr/webpack を利用しましたが、Next.jsとStorybookでWebpackの設定が異なってしまうのは気になっています。Webpackで複雑なことをするとViteなどへの乗り換えも難しくなるため可能であれば最小限にしたいとも思います。
より良い選択肢があれば別の方法に乗り換えたいところです。





Discussion