Storybook と Ladle のセットアップを比較してみる

## この記事の概要

個人的な開発において「コンポーネントカタログが欲しい。とは言え、表示確認だけできれば良くて、手早く準備できて軽いものが欲しい。」という場面が結構あります。
こういう場面では、わざわざ Storybook を使うほどでもないなあ……と感じてしまっていました。

というわけで、Alternative Storybook として名前だけ知っていた Ladle を試してみました。

https://github.com/tajo/ladle

## React の準備

ひとまず比較のために Storybook をセットアップしたいのですが、`storybook init` コマンドは空のプロジェクトでは実施できません。
そのため簡単に React を準備します。

```sh
bun init
```

```sh
bun i react react-dom
```

```sh
bun i -d @types/react @types/react-dom
```

現在のファイル構成などは以下です。

```
.
├── README.md
├── bun.lockb
├── index.ts
├── package.json
└── tsconfig.json
```

```json:package.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 のセットアップへ移ります。

```sh
bunx storybook@latest init
```

2, 3分待った後、無事に起動しました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/dd656a3b-6cac-3307-325b-07c83c0fb4d5.png)

現在のファイル構成などは以下です。

```
.
├── 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
```

```json:package.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 を準備した段階に戻って、セットアップを進めます。

```sh
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

公式ドキュメントの書き方からは少し変わりましたが、このようにしたら動きました。

```tsx:Button.stories.tsx
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>>;` と取得して事なきを得ました。

https://ladle.dev/docs/controls

全体的に Storybook 6.x 系の書き方に似ている気がします。

https://storybook.js.org/docs/6.5/get-started/setup

### Header

`Button` とほぼ変わりませんので説明を省略します。

```tsx:Header.stories.tsx
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 の何かでエラーが起きて動かせませんでした……。

悔しさはありますが、今回は諦めました。

```tsx:Page.stories.tsx
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` のようなフォルダが不要なのも嬉しいポイントでした。

見た目が簡素ではありますが、↑の要望があるのに見た目にはリッチさを求めるのも主張が一貫していない感じがするので、良しとしています。

公式ドキュメント以外の情報がほぼ皆無なことから業務で使うのは怖いですが、個人開発でなら次も使ってみようと思いました。