この記事の概要
個人的な開発において「コンポーネントカタログが欲しい。とは言え、表示確認だけできれば良くて、手早く準備できて軽いものが欲しい。」という場面が結構あります。
こういう場面では、わざわざ Storybook を使うほどでもないなあ……と感じてしまっていました。
というわけで、Alternative Storybook として名前だけ知っていた Ladle を試してみました。
React の準備
ひとまず比較のために Storybook をセットアップしたいのですが、storybook init
コマンドは空のプロジェクトでは実施できません。
そのため簡単に React を準備します。
bun init
bun i react react-dom
bun i -d @types/react @types/react-dom
現在のファイル構成などは以下です。
.
├── README.md
├── bun.lockb
├── index.ts
├── package.json
└── tsconfig.json
{
"name": "comparing-storybook-ladle",
"module": "index.ts",
"type": "module",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}
Storybook
今回はアプリケーション自体を作るつもりはないので、このまま Storybook のセットアップへ移ります。
bunx storybook@latest init
2, 3分待った後、無事に起動しました。
現在のファイル構成などは以下です。
.
├── README.md
├── bun.lockb
├── index.ts
├── package.json
├── stories
│ ├── Button.stories.ts
│ ├── Button.tsx
│ ├── Configure.mdx
│ ├── Header.stories.ts
│ ├── Header.tsx
│ ├── Page.stories.ts
│ ├── Page.tsx
│ ├── assets
│ │ ├── 省略
│ ├── button.css
│ ├── header.css
│ └── page.css
└── tsconfig.json
{
"name": "comparing-storybook-ladle",
"module": "index.ts",
"type": "module",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.6.4",
"@storybook/addon-interactions": "^7.6.4",
"@storybook/addon-links": "^7.6.4",
"@storybook/addon-onboarding": "^1.0.9",
"@storybook/blocks": "^7.6.4",
"@storybook/react": "^7.6.4",
"@storybook/react-vite": "^7.6.4",
"@storybook/test": "^7.6.4",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"bun-types": "latest",
"storybook": "^7.6.4"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
}
}
自動生成のあれこれを init コマンドが担ってくれるのは便利ですが、3 つのコンポーネントと 1 つのドキュメントを用意するだけにしては時間がかかる印象です。
また、私は「依存関係は少なければ少ないほど嬉しい」という派閥なので、一度にたくさん入り過ぎて少し気圧されています。
オンボーディングのための @storybook/addon-onboarding
なんかも、勝手に入れないでくれという気持ちです。
Ladle
ここからが本題です。
Ladle のセットアップを試してみます。
せっかくなので(?) storybook init
で生成されるものと同じものを表示できるようにしていみます。
React を準備した段階に戻って、セットアップを進めます。
bun i @ladle/react
storybook init
で生成される、以下の 4 つを表示するまでをやってみます。
- Button.stories.ts
- Header.stories.ts
- Page.stories.ts
- Configure.mdx
共通すること - 拡張子の変更
Storybook は拡張子が .ts
で大丈夫ですが、Ladle は .tsx
にしないとコンポーネントが動きませんでした。
また、.mdx
も .stories.mdx
にしないといけませんでした。
Button
公式ドキュメントの書き方からは少し変わりましたが、このようにしたら動きました。
import type { StoryDefault, Story } from "@ladle/react";
import { Button } from "./Button";
type StoryType = Story<React.ComponentProps<typeof Button>>;
const ButtonStory: StoryType = (props) => <Button {...props} />;
ButtonStory.storyName = "Button";
export default {
args: {
primary: false,
size: "medium",
label: "Button",
},
argTypes: {
backgroundColor: {
control: {type: "color"},
},
size: {
control: {type: "select"},
options: ["small", "medium", "large"],
},
}
} satisfies StoryDefault
export const Primary = ButtonStory.bind({});
Primary.args = {
primary: true,
};
export const Secondary = ButtonStory.bind({});
export const Large = ButtonStory.bind({});
Large.args = {
size: "large",
};
export const Small = ButtonStory.bind({});
Small.args = {
size: "small",
};
公式で紹介されている内容は、別ファイルで定義したコンポーネントを import するのではなく、stories ファイルの中ではじめてマークアップを書いているようなものが多かったです。
紹介されているまま真似すると props の型を 2 回書くようなハメになりそうで、type StoryType = Story<React.ComponentProps<typeof Button>>;
と取得して事なきを得ました。
全体的に Storybook 6.x 系の書き方に似ている気がします。
Header
Button
とほぼ変わりませんので説明を省略します。
import type { Story } from "@ladle/react";
import { Header } from "./Header";
type StoryType = Story<React.ComponentProps<typeof Header>>;
const HeaderStory: StoryType = (props) => <Header {...props} />;
HeaderStory.storyName = "Header";
export const LoggedIn = HeaderStory.bind({});
LoggedIn.args = {
user: { name: "Jane Doe" },
};
export const LoggedOut = HeaderStory.bind({});
Page
storybook init
で作られた Page
は play
関数を通してログイン状態を表示するものでした。
Ladle でも Playwright を使ってできそうだったのですが、Ladle の依存関係の中の Vite と fsevents の何かでエラーが起きて動かせませんでした……。
悔しさはありますが、今回は諦めました。
import type { Story } from "@ladle/react";
import { Page } from "./Page";
type StoryType = Story<React.ComponentProps<typeof Page>>;
export const PageStory: StoryType = (props) => <Page {...props} />;
PageStory.storyName = "Page";
Configure
拡張子さえ変えれば無事に表示できました。
比較してみて
起動の速さや依存関係の少なさなど、記事冒頭に掲げた「表示確認だけできれば良くて、手早く準備できて軽いものが欲しい。」という目的はかなり叶えられると思います。
コンフィグファイルも不要ですし、.ladle
のようなフォルダが不要なのも嬉しいポイントでした。
見た目が簡素ではありますが、↑の要望があるのに見た目にはリッチさを求めるのも主張が一貫していない感じがするので、良しとしています。
公式ドキュメント以外の情報がほぼ皆無なことから業務で使うのは怖いですが、個人開発でなら次も使ってみようと思いました。