3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

完走賞を目指す @xrxoxcxoxAdvent Calendar 2023

Day 8

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

Last updated at Posted at 2023-12-09

この記事の概要

個人的な開発において「コンポーネントカタログが欲しい。とは言え、表示確認だけできれば良くて、手早く準備できて軽いものが欲しい。」という場面が結構あります。
こういう場面では、わざわざ 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
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 のセットアップへ移ります。

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

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

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

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>>; と取得して事なきを得ました。

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

Header

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

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 で作られた Pageplay 関数を通してログイン状態を表示するものでした。
Ladle でも Playwright を使ってできそうだったのですが、Ladle の依存関係の中の Vite と fsevents の何かでエラーが起きて動かせませんでした……。

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

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

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

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?