🦔

[@svgr/webpack] Next.js / Storybook でSVGをコンポーネントとして扱う

2022/05/07に公開

やりたいこと

  • リポジトリにSVGファイルを格納して、Reactコンポーネントから扱いたい
  • Next.jsとStorybook両方で表示したい

記事要約

  • @svgr/webpackを利用し、webpackの設定を書いて解決する
  • Next.jsで表示するのは簡単だが、Storybookでは多くの罠があった

前提情報

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:
https://github.com/gregberge/svgr/issues/631

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:

https://github.com/storybookjs/storybook/issues/15336#issuecomment-906809203

./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でロードされるようになっているのが原因でした。

https://github.com/storybookjs/storybook/issues/9070

それを打ち消すように設定すれば解決しました。

  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などへの乗り換えも難しくなるため可能であれば最小限にしたいとも思います。
より良い選択肢があれば別の方法に乗り換えたいところです。

GitHubで編集を提案

Discussion