バレルファイルで共通コンポーネントを作ったがimportが思った通りに動かなかった
はじめに
個人的なプロジェクトでは、モノレポ構成を採用することが多いです。そして共通コンポーネントをそれぞれのアプリケーションで使用する様にしています。
今回のプロジェクトではViteを使用してビルドし、shadcn/uiを利用してコンポーネントを作成しています。
また、1つのエントリーポイントでビルドして共通コンポーネントを作成し、それをそれぞれのアプリケーションで利用するという構成です。
/
├─ apps
│ ├─ applicationA // ここでButtonのみを呼び出す
│ └─ applicationB // ここで共通コンポーネントを色々呼び出す
├─ packages
│ └─ typescript
│ └─ ui // ここが共通コンポーネント
│ ├─ src
│ │ └─ components
│ │ └─ ui
│ │ ├─ button.tsx
│ │ ├─ calendar.tsx
│ │ ├─ select.tsx
│ │ └─ ...
│ ├─ package.json
│ └─ vite.config.ts
└─ ...
しかし、この方法には予期せぬ落とし穴があることが分かりました。
本記事では、バレルファイルを用いた共通コンポーネントの問題点と、それに代わる方法について説明します。
バレルファイルとは
バレルファイルは、複数のモジュールをまとめて一つのエントリーポイントとして提供するファイルです。通常、index.ts
やindex.js
という名前で作成され、以下のような形式になります:
export * from './src/components/button.tsx'
export * from './src/components/calendar.tsx'
export * from './src/components/select.tsx'
// ... その他のコンポーネント
この方法により、インポート文を簡潔にでき、コードの見通しが良くなると考えられています。
import { Button, Calendar } from 'path/to/index.ts'
問題の発生
applicationAをSSGでビルドする際に問題が発生しました。
このapplicationAではshadcn/uiで作成したButtonコンポーネントのみを使用しています。ButtonコンポーネントはServer Componentです。
このapplicationAをSSGビルドする際に以下の様なエラーが発生しました。
You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
つまりuseStateはクライアントのみで動くが、use clientがないためServerComponentになっているのでuse clientをつけてください。といった内容のエラーです
しかし、ButtonコンポーネントuseStateを使っていないしServerComponentで、SSGでビルドする際はuse clientなどをつけなくても問題ないはずです。なぜそのようなエラーが起こったのでしょうか?
原因はそうバレルファイルです。
バレルファイルを使用することで、Tree shakingが機能せず、共通コンポーネントの全てのコンポーネントがバンドルされてしまいます。その結果、useStateを使用しているコンポーネントも一緒に読み込まれ、エラーが発生していました。
Tree shakingとは、不要なコードを削除する機能のことで、この文脈ではインポートしていないコンポーネントをビルドに含めないようにする機能です。
Tree shakingがうまく機能していればButtonコンポーネントのみが読み込まれ、useStateを使用しているコンポーネントがビルドに含まれず、SSGで正常にビルドできる様になります。
解決方法
解決方法は単純でバレルファイルを使わないことです。具体的なpacakges/typescript/ui/vite.config.ts
の設定は以下のようになります。
import glob from "fast-glob"
import { defineConfig, loadEnv } from "vite"
type Mode = "development" | "production" | "analyze"
// components配下のtsxファイルをエントリーポイントとして設定
const files = glob.sync(["./src/components/**/*.{ts,tsx}"]).map((file) => {
const fileName = file.split("/").pop()?.split(".")[0]
return [fileName, file]
})
const entries = Object.fromEntries(files)
export default ({ mode }: { mode: Mode }) => {
// ... 省略 ...
return defineConfig({
build: {
// ... その他の設定 ...
lib: {
entry: entries
}
},
// ... その他の設定 ...
})
}
上記の設定により、src配下の各コンポーネントが個別のファイルとしてdistディレクトリに出力されます。
/
├─ pacakges
│ └─ typescript
│ └─ ui
│ ├─ src
│ │ └─ components
│ │ └─ ui
│ │ ├─ button.tsx
│ │ ├─ calendar.tsx
│ │ ├─ select.tsx
│ │ └─ ...
│ ├─ dist // ビルド出力先のディレクトリ
│ │ └─ components
│ │ └─ ui
│ │ ├─ button.cjs
│ │ ├─ calendar.cjs
│ │ ├─ select.cjs
│ │ └─ ...
│ ├─ package.json
│ └─ vite.config.ts
└─ ...
これにより、import側では以下のように個別にコンポーネントをインポートできます
import { Button } from "shared/components/ui/button"
import { Select } from "shared/components/ui/select"
この変更後、applicationAを再度SSGでビルドしたところ、エラーが解消され、Tree shakingが正常に機能することが確認できました。
まとめ
バレルファイルは基本的には使わない方がよさそう。
おまけ
この記事で紹介したプロジェクトは、以下のサイトで公開しています。
広告なしでルーレットやグループ分けができるツールを提供しています。
ぜひ遊んでみてください🙏
Discussion