🥨

Remix v2 と組み合わせて Ladle を使ってみる

2024/12/18に公開

はじめに

この記事は「コネヒト Advent Calender 2024」の17日目の記事です。
https://adventar.org/calendars/10480

Ladle とは?

Storybookにインスパイアされたコンポーネントカタログのライブラリで、ReactベースのJavaScript / TypeScriptのプロジェクト上で使用することができます。
Storybookと同じような書き味のstoriesファイルを用いてコンポーネントのカタログを作成できるほか、Viteなどのビルドツールにも対応しており、MSWやPlaywrightを用いたスナップショットテストも実行可能です。
Storybookとの相違点で軽量(約250KB)・ゼロコンフィグを謳っており、規模の小さいプロジェクトでコンポーネントのプレビューを主に使いたいけどStorybookの設定がタスクとして重い・・・といったシーンで有用かもしれないと思い今回使用を試みました。
https://ladle.dev/

Remix(v2) への対応状況

現在、公式では React, Next.jsへのサポートは明示的に公式のDocsで提供していますが、Remixに関してはまだ対応の準備が整っていないようです。しかし、GitHubのIssueで使用できそうな手がかりを見つけたので、この記事で導入手順を解説したいと思います。

Remix プロジェクトのセットアップ

まずは、Remix のプロジェクトを1からセットアップしたいと思います。pnpm 経由で以下のコマンドを実行すれば初期設定は完了します。

// pnpmを導入する場合はここから
npm i -g pnpm

pnpm create remix@latest example-remix-ladle

pnpm run dev

Remix の初期プロジェクトが立ち上がればOKです。

ladle の導入と設定

pnpm 経由では以下のコマンドで ladle をインストールできます。

 pnpm add @ladle/react

Remix では Vite の設定ファイルをサードパーティのプラグインと分けないとコンフリクトがおこり、アプリケーション側が立ち上がらない場合があります。このコンフリクトを解決するために、vite.ladle.config.tsというファイルをプロジェクト直下に新たに作成し、大元のコンフィグファイルを元に以下のようにファイル全体を書き換えます。

import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

declare module "@remix-run/node" {
  interface Future {
    v3_singleFetch: true;
  }
}

export default defineConfig({
  plugins: [
    tsconfigPaths(),
  ],
});

また、Ladle のデフォルトの起動コマンドはpnpm ladle serveですが、上記のコンフィグファイルを使って起動できるようにpackage.jsonに起動用のコマンドを新たに定義します。

"scripts": {
    "build": "remix vite:build",
    "dev": "remix vite:dev --port 5170",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve ./build/server/index.js",
+   "ladle": "ladle serve --viteConfig vite.ladle.config.ts",
    "typecheck": "tsc"
  },

以上の設定を終えた後、pnpm ladleを実行するとアプリケーションと Vite がコンフリクトを起こさず Ladle のトップページにアクセスできるようになります。デフォルトでのポート番号はhttp://localhost:61000/となっています。
現時点では stories ファイルを用意していないため、「No stories found」と表示されます。

stories の追加

ここから簡単なコンポーネントを JSX + CSS Modules の組み合わせで作り、それに対応した stories ファイルを追加していきたいと思います。
Ladle のルールとして、プロジェクト直下の src/ディレクトリの内部に**.stories.(tsx|jsx)を配置する必要があります。(本当はこのパスの設定をappディレクトリなどに変更したいのですが、Remixのプロジェクトだと試行錯誤したものの変更できませんでした。)

Button コンポーネントの作成

今回は Default と Disable の状態を切り替えられるボタンで試してみました。
app/ディレクトリに新たにcomponents/Buttonsディレクトリを切って、Button.tsxButton.module.cssを以下のように作成しました。

import React from 'react';
import styles from './Button.module.css';

type ButtonProps = {
  onClick: () => void;
  children: string;
  variant: 'default' | 'disabled';
}

const Button: React.FC<ButtonProps> = (props) => {
  const { children, variant, onClick } = props;
  const className = variant === 'default' ? styles.default : styles.disabled;

  return (
    <button className={className} onClick={onClick}>
      {children}
    </button>
  )
};

export default Button;
.default {
  background-color: green;
  color: white;
  height: 40px;
  width: 120px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.disabled {
  background-color: lightgreen;
  color: white;
  height: 40px;
  width: 120px;
  border: none;
  border-radius: 5px;
  cursor: not-allowed;
}

stories ファイルの作成

次に、Ladle が参照する stories を作成します。
src/storiesディレクトリを作成し、以下のようなファイルを作成して動作確認をしました。1つの stories ファイルの中で2つのコンポーネントを export する形式です。

import type { Story } from "@ladle/react";
import Button from "../../app/component/Buttons/Button";

export const DefaultButton: Story = () => (
  <Button onClick={() => console.log("Button clicked")} variant="default">Button</Button>
);

export const DisabledButton: Story = () => (
  <Button onClick={() => console.log("Button clicked")} variant="disabled">Button</Button>
);

再度 pnpm ladle で Ladle を立ち上げてみると、左のメニューバーにButtonsディレクトリと2つのコンポーネントの stories が追加されていることを確認できました。画面下のメニューバーでダークモードへの切り替え・viewport の変更や、コンポーネントのコードをブラウザ上に出力する機能も備わっています。

appendix

コンポーネントごとに階層を区切ったりする際はオリジナルの命名規則があるので、こちらのDocsを参照してください。
https://ladle.dev/docs/stories

違うパターンで stories ファイルを書いてみる

次に、同じ画面上で複数パターンのコンポーネントを切り替えてプレビュー可能な stories を作成してみたいと思います。下記のコードを参照してもらうとわかるのですが、かなり Storybook と似通った形で stories を書くことが可能です。
argTypesoptionsでコンポーネントのパターンを追加し、セレクトボックス形式でコンポーネントの表示を切り替えられるようcontroltype: "select"を指定しました。その他にも使えるcontrolの種類については下記のDocsを参照してください。
https://ladle.dev/docs/controls

import type { Story, StoryDefault } from "@ladle/react";
import Button from "../../app/component/Buttons/Button";

type ButtonProps = React.ComponentProps<typeof Button>;

export default {
  title: "Components/Button",
  args: {
    onClick: () => console.log("Button clicked"),
    children: "Button",
  },
  argTypes: {
    variant: {
      options: ["default", "disabled"],
      control: { type: "select" },
      defaultValue: "default",
    },
  },
} satisfies StoryDefault<ButtonProps>;

const ButtonStory: Story<ButtonProps> = (props) => <Button {...props} />;

export const Buttons = ButtonStory.bind({});

上記のファイルを作成し、再度pnpm serveを実行してみましょう。メニューバーに新たに Components, Button の階層が追加され、コンポーネントがプレビューできるようになりました。

左下のメニューバーの一番左に、コントロール用のボタンが表示されるようになりました。ここから Button の種類を変更できる他、表示する文字列も自由に置き換えてプレビューできるようになり、カタログをよりインタラクティブに使用できるようになりました。

所感

Storybook は日々進化を続けており、メジャーアップデートがあるたび新たな機能が追加され、複数人・大規模なフロントエンド開発でのプロジェクトでは様々なシーンで恩恵に与れると思っています。
一方で、コンポーネントカタログのライブラリは他に選択肢が少なく、導入やアップデートをするだけで工数を費やしがちで負担を感じている開発者も一定いるのではないか、と考えています。
反面、Ladle はまだ導入事例が少なく知見もフロントエンド界隈で溜まってはいないものの、最低限の機能を持ったコンポーネントのプレビューライブラリとして十分活用できそうです。

参考記事

https://zenn.dev/chot/articles/0ddde94eb589c6

https://dev.classmethod.jp/articles/react-vite-ladle/

Discussion