👏

Suspense ありのコンポーネントでも Storybook を使いたい話

に公開

🌼 はじめに

現在 Next.js の App Router を使って開発しており、Suspense でパフォーマンス改善を試したところ、Storybook がエラーで動かなくなりました。

結構苦労して解決したので、その話をしたいと思います。

1. Storybook エラーの原因

Storybook エラーの原因は結論から言うと、Storybook がブラウザランタイムでは動かないライブラリを読み込んだことでした。

今のプロジェクトでは OpenSearch と Prisma でデータフェッチをしているのですが、この子たちは Node.js に依存していて、ブラウザランタイムでは動かないです。

でも Suspense を使うと、データフェッチ処理がコンポーネント内に入ることになり、結果として Storybook(=ブラウザランタイム)でそれらのライブラリを読み込もうとしてしまいます。

当然、動きません。そして、以下のようなエラーが発生します。

OpenSearch の場合

Failed to build the preview
SB_BUILDER-WEBPACK5_0002 (WebpackInvocationError): 
Module not found: Error: Can't resolve 'v8' in './node_modules/@opensearch-project/opensearch/lib'

Prisma の場合

Error: Prisma Client cannot run in the browser.

そうやって Storybook のビルドが失敗し続きました(私の苦しみも続く)。

2. モックで解決

2-1. ライブラリをモック

でもなんとかしました。

「ライブラリが原因なら、、ライブラリごとモックしちゃえ!」という気持ちで、ライブラリを空のファイルに差し替える作戦です。

そのためにまず空のモックファイルを作りました。

↓ OpenSearch 用のモック

.storybook/__mocks__/opensearch.ts
export class Client {}

export default { Client }

↓ Prisma 用のモック

.storybook/__mocks__/prisma.ts
const PrismaClientMock = class {
  $extends() {}
}

// NOTE: readReplicas のダミーモジュール
const readReplicasMock = () => ({
  url: ['mocked_url'],
})

module.exports = {
  PrismaClient: PrismaClientMock,
  readReplicas: readReplicasMock,
}

エラーメッセージにどのライブラリが原因かが書かれているので、ひとつ解決したらまた別のライブラリでエラーが出て…という感じで、地味にモックファイルを増やしていきました。

次に、Webpack の alias 機能を使ってライブラリの import パスをモックファイルに差し替えます。

.storybook/main.ts
config.resolve = config.resolve || {}
config.resolve.alias = {
  ...config.resolve.alias,
  '@opensearch-project/opensearch': path.resolve(__dirname, './__mocks__/opensearch.ts'),
  '@prisma/client': path.resolve(__dirname, './__mocks__/prisma.ts'),
  '@prisma/extension-read-replicas': path.resolve(__dirname, './__mocks__/prisma.ts'),
}

これで Storybook 上では、本物のライブラリではなく、モックファイルを読み込むようになります。

これで Storybook のエラーは解消!

2-2. データフェッチをモック

ここまででエラーは解消されましたが、今度は表示するダミーデータが必要になります。

そこで登場するのが storybook-addon-module-mock です。
https://github.com/ReactLibraries/storybook-addon-module-mock

これを使えば、特定のモジュールや関数を Storybook 上だけモックできます。

使い方は github にのってるのでそのままにすればすぐ使えるます。

まずは Storybook の addon に storybook-addon-module-mock を追加して、

.storybook/main.ts
const config: StorybookConfig = {
  // ...
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    {
      name: 'storybook-addon-module-mock',
      options: {
        exclude: ['**/node_modules/**'],
      },
    },
  ],
};

モックしたいモジュールの返り値を指定します。

parameters: {
  moduleMock: {
    mock: () => {
      const mockGetProduct = createMock(product, 'getProduct')
      mockGetProduct.mockResolvedValue({
        name: 'プロダクト',
        price: '12000',
      })
      return [mockGetProduct]
    },
  },
},

これで(やっと)Suspense で非同期のデータフェッチをしているコンポーネントでも、Storybook 上ではモックされたデータを返せるようになります。

🌷 終わり

なんか書いてみたら簡単ですけど、このモックを成功させるまで2日以上かかりました…笑

Storybook もっとサクッとモックできるようにしてくれないかな

GitHubで編集を提案

Discussion