ClaudeExpo2025/09/25 17:45

Building beautiful components faster with Storybook 9 and Expo

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

claudejamodel: claude-sonnet-4-20250514

Storybook 9とExpoを使ったReact Nativeコンポーネント開発の効率化

Key Points

  • Storybook 9のExpo統合でコンポーネント分離開発が可能
  • Metro設定とExpo Routerでストーリーブック環境を構築
  • 環境変数による表示制御とTestFlightでのチーム共有

Storybook 9をExpoアプリに統合してReact NativeのUIコンポーネントを効率的に開発する方法を解説。Metro設定の調整、Expo Routerでのストーリーブック用ルート作成、コンポーネントのストーリー作成、デコレーターの活用、環境変数を使った表示制御などの実装手順を詳細に説明。コンポーネントの分離開発とチーム共有を実現する実践的なワークフローを提供。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

claudejamodel: claude-sonnet-4-20250514

Storybook 9とExpoでより美しいコンポーネントをより速く構築する

Storybook 9とExpoでより美しいコンポーネントをより速く構築する

Development • React Native • 2025年9月25日 • 12分で読める

Daniel Williams ゲスト著者

ExpoアプリでStorybook 9をセットアップして、React Native UIコンポーネントをより速く、より少ない摩擦で構築、テスト、共有する方法を学びましょう。

これは、ReactとReact Nativeで10年以上の経験を持つフロントエンド開発者でオープンソースメンテナーのDaniel Williamsによるゲストブログ投稿です。


次のような状況を想像してみてください:React Nativeアプリの奥深くにある複雑な画面を構築していて、様々な成功状態とエラー状態があります。変更によってフルリロードがトリガーされるたびに、アプリにログインして作業していた状態に戻る必要があります。最初の数回はそれを受け流しますが、このワークフローはすぐに疲れてしまいます!

代わりに、各UI状態のカタログがあったらどうでしょうか?変更前に中断した場所から直接その画面に移動できるだけでなく、簡単にレビューできるようにチームメイトとそれらの画面を共有することもできます。さらに、エンドツーエンドテストの不安定さなしに、CIで迅速に実行される自動テストにそれらのUI状態を変換することもできます。

ここで説明しているワークフローは、まさにStorybook for React Nativeが目的としているものです。この投稿では、Expoアプリでそれをセットアップして最大限に活用する方法をガイドします。この投稿は、これまでで最高のReact Nativeサポートを提供する最近リリースされたStorybook 9に基づいています。

ExpoアプリでStorybook 9をセットアップする

Storybookが初めての方のために説明すると、これはUIコンポーネントを分離して構築、テスト、文書化できる開発環境です。完全なアプリケーションの複雑さなしに、各コンポーネントを独立して作成できるワークショップと考えてください。このコンポーネントファーストのワークフローは、ビジュアルテストを可能にし、生きたドキュメントとして機能し、大規模なコードベース全体でデザインの一貫性を維持するのに役立ちます。

始める前に、以下が必要です:

  • Expo Routerを使用する既存のExpoアプリ(SDK 50以降を推奨)
  • または、一緒に進めるためにnpx create-expo-appで新しいものを作成
  • 新しいexpoアプリを作成する場合は、npm run reset-projectを実行してクリーンなプロジェクトを取得
  • Node.js 20+と好みのパッケージマネージャー(bun、npm、yarn、またはpnpm)
  • テスト用のデバイスまたはシミュレーター

このガイドのstorybookの完成版を確認したい場合は、サンプルリポジトリで見つけることができます。

Storybookのインストール

既存のExpo RouterプロジェクトにStorybookを追加する最も簡単な方法は、createコマンドです:

npm create storybook@latest

プロンプトが表示されたら、recommendedを選択し、次にReact Nativeを選択します。これにより、.rnstorybook設定フォルダが作成され、@storybook/react-nativeを含む必要な依存関係がインストールされます。

Metroバンドラーの設定

Storybookで動作するためにいくつかのMetro設定が必要です。まず、Metro設定をカスタマイズします:

npx expo@latest customize metro.config.js

次に、Storybookサポートを含むようにmetro.config.jsを更新します:

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

const withStorybook = require("@storybook/react-native/metro/withStorybook");

/** withStorybook Adds the config that storybook uses */
module.exports = withStorybook(config);

Storybookルートの作成

Expo Routerで、app/storybook.tsxを追加してStorybookの新しいルートを作成します:

export { default } from '../.rnstorybook';

これにより、アプリ内で移動できる/storybookルートが作成されます。

storybookルートを設定してヘッダーを非表示にし、storybookを開発時のみアクセス可能にします(または独自のロジックに基づいて)。これを行うには、レイアウトファイルapp/_layout.tsxを次のように編集します:

// app/_layout.tsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="index" />
      <Stack.Protected guard={__DEV__}>
        <Stack.Screen name="storybook" />
      </Stack.Protected>
    </Stack>
  );
}

ここでStack.Protectedを使用することで、画面は開発時のみアクセス可能になります。環境変数や他のロジックを使用してこの動作を変更することで拡張できます。

Storybookの実行

残りはアプリを実行するだけです:

npm run start

次に、アプリの/storybookルートへのリンクを追加してstorybookにアクセスします。今のところ、インデックスapp/index.tsxに追加しましょう:

import { Link } from "expo-router";
import { View } from "react-native";

export default function Index() {
  return (
    <View
      style={{
        flex: 1,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <Link href="/storybook">Open Storybook</Link>
    </View>
  );
}

storybook画面を開くと、次のようなものが表示されるはずです:

コンポーネントのストーリーを書く

Storybookでは、ストーリーは特定のUI状態でのコンポーネントの例です。例えば、各状態でボタンを表示するLoadingとDisabledストーリーを作成できます。

異なるStorybook機能を紹介するために、入力コンポーネントを構築しましょう。

まもなく新しいコンポーネントとストーリー用のcomponentsフォルダを作成します。storybookがそのフォルダ内のストーリーを取得するために、.rnstorybook/main.tsのストーリー正規表現にcomponentsディレクトリを追加する必要があります:

// .rnstorybook/main.ts
import { StorybookConfig } from "@storybook/react-native";

const main: StorybookConfig = {
  stories: [
    "./stories/**/*.stories.?(ts|tsx|js|jsx)",
    // the paths are relative to the main.ts file itself
    "../components/**/*.stories.?(ts|tsx|js|jsx)", // <--- Add this
  ],
  addons: [
    "@storybook/addon-ondevice-controls",
    "@storybook/addon-ondevice-actions",
  ],
};

export default main;

🚨 重要な注意:ストーリー正規表現を変更した後は、必ずmetroを再起動してください。これにより、storybook.requires.tsファイルの生成がトリガーされ、Storybookが含めた新しいディレクトリを監視できるようになります。

いくつかの状態を持つシンプルな入力コンポーネントを作成しましょう。componentsディレクトリにinput.tsxファイルを追加します(まだ作成していない場合はディレクトリを作成してください):

// components/input.tsx
import { useState } from "react";
import { Text, TextInput, View, type TextInputProps } from "react-native";

type InputProps = TextInputProps & {
  label?: string;
  error?: string;
  disabled?: boolean;
};

const getBorderColor = (isFocused: boolean, error?: string) => {
  if (error) {
    return "#FF3B30";
  }
  return isFocused ? "#007AFF" : "#D1D1D6";
};

export const Input = ({ disabled, label, error, ...props }: InputProps) => {
  const [isFocused, setIsFocused] = useState(false);
  const borderColor = getBorderColor(isFocused, error);

  return (
    <View style={{ gap: 4 }}>
      <Text id="input-label" style={{ fontSize: 14, color: "#3C3C43" }}>
        {label}
      </Text>
      <TextInput
        aria-labelledby="input-label"
        aria-disabled={disabled}
        style={{
          borderWidth: 1,
          padding: 12,
          borderRadius: 8,
          borderColor,
          backgroundColor: disabled ? "#F5F5F5" : "transparent",
        }}
        editable={!disabled}
        onFocus={() => {
          setIsFocused(true);
        }}
        onBlur={() => {
          setIsFocused(false);
        }}
        {...props}
      />
      {error && <Text style={{ fontSize: 12, color: "#FF3B30" }}>{error}</Text>}
    </View>
  );
};

componentsディレクトリに次のようなinput.stories.tsxファイルを追加します:

// components/input.stories.tsx
import { Meta, StoryObj } from "@storybook/react-native";
import { Input } from "./input";

const meta = {
  component: Input,
} satisfies Meta<typeof Input>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic: Story = {
  args: {
    label: "First Name",
    placeholder: "John",
  },
};

これで基本的なストーリーができ、"First Name"ラベルと"John"プレースホルダーを持つ入力がstorybookに表示されます。ストーリーのargsは、ストーリーがレンダリングされるときにコンポーネントに渡されるデフォルトのpropsを設定し、コントロールパネルを使用してリアルタイムで状態を調整できます。

次に、ErrorとDisabledストーリーを追加して、ストーリーファイルにいくつかの状態を追加できます:

// components/input.stories.tsx
// ... rest of the story file 👆

export const Error: Story = {
  args: {
    label: "Email",
    error: "Email is required",
    disabled: false,
    placeholder: "example@example.com",
  },
};

export const Disabled: Story = {
  args: {
    label: "Disabled",
    error: "",
    disabled: true,
    placeholder: "Disabled",
  },
};

これで、メニューに新しい状態が表示され、それらの間を素早く移動できます。

入力コンポーネントがUIの側面にぴったりと配置されていて、周りにパディングを追加できればより良いかもしれないことに気づくかもしれません。これはまさにデコレーターが使用できる種類のものです。ストーリーファイルのメタを編集してデコレーターを追加しましょう:

import { View } from "react-native"; // add this

// components/input.stories.tsx
const meta = {
  component: Input,
  decorators: [
    // Here we're adding a container with 16px padding.
    // add this 👇
    (Story) => (
      <View style={{ padding: 16 }}>
        <Story />
      </View>
    ),
  ],
} satisfies Meta<typeof Input>;

// Rest of your stories file here...

これで、コンポーネントの周りにもう少しスペースがあることがわかるはずです。デコレーターは、テーマ、モック、またはアプリケーション状態プロバイダーなどにも使用できます。個別のストーリーのStoryレベルまたは.rnstorybook/preview.tsxでグローバルにデコレーターを追加することもできます。

const meta = {
  component: Input,
  decorators: [
    // You can wrap your stories here with anything you like
    // Here I added an example ThemeProvider that could come from a UI library
    (Story) => (
      <ThemeProvider>
        <View style={{ padding: 16 }}>
          <Story />
        </View>
      </ThemeProvider>
    ),
  ],
} satisfies Meta<typeof Input>;

ストーリーの書き方の詳細については、公式ドキュメントをご覧ください。

Storybookを共有する

Storybookはコンポーネントを分離して開発するのに優れていますが、もう一つの超能力は、チームメイトとコンポーネントを共有できることです。

先に進む前に、storybookを有効/無効にする方法をセットアップしましょう。EXPO_PUBLIC_ENVIRONMENTstorybookに設定されている場合のみstorybookを表示するように_layoutファイルを編集します:

// app/_layout.tsx
export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="index" />
      <Stack.Protected guard={process.env.EXPO_PUBLIC_ENVIRONMENT === "storybook"}>
        <Stack.Screen name="storybook" />
      </Stack.Protected>
    </Stack>
  );
}

次に、環境をstorybookに設定して実行するスクリプトをpackage.jsonに追加できます:

"storybook": "EXPO_PUBLIC_ENVIRONMENT='storybook' expo start",

これで、storybookでコンポーネントを開発したい場合はnpm run storybookを実行し、通常通りアプリを開発するにはnpm run startを実行できます。

今のところアプリには他に何も入れておらず、今はそれに焦点を当てませんが、環境がstorybookでない場合はstorybookへのリンクを非表示にできます。

TestFlightでiOS向けに共有

TestFlightを使用するにはApple開発者アカウントが必要です。これにより以下の機能が提供されます

Storybook 9 と Expo でより速く美しいコンポーネントを構築する | Expo | DocsDigest