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を更新します:
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
const withStorybook = require("@storybook/react-native/metro/withStorybook");
module.exports = withStorybook(config);
Storybookルートの作成
Expo Routerで、app/storybook.tsxを追加してStorybookの新しいルートを作成します:
export { default } from '../.rnstorybook';
これにより、アプリ内で移動できる/storybookルートが作成されます。
storybookルートを設定してヘッダーを非表示にし、storybookを開発時のみアクセス可能にします(または独自のロジックに基づいて)。これを行うには、レイアウトファイル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ディレクトリを追加する必要があります:
import { StorybookConfig } from "@storybook/react-native";
const main: StorybookConfig = {
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
],
addons: [
"@storybook/addon-ondevice-controls",
"@storybook/addon-ondevice-actions",
],
};
export default main;
🚨 重要な注意:ストーリー正規表現を変更した後は、必ずmetroを再起動してください。これにより、storybook.requires.tsファイルの生成がトリガーされ、Storybookが含めた新しいディレクトリを監視できるようになります。
いくつかの状態を持つシンプルな入力コンポーネントを作成しましょう。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ファイルを追加します:
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ストーリーを追加して、ストーリーファイルにいくつかの状態を追加できます:
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";
const meta = {
component: Input,
decorators: [
(Story) => (
<View style={{ padding: 16 }}>
<Story />
</View>
),
],
} satisfies Meta<typeof Input>;
これで、コンポーネントの周りにもう少しスペースがあることがわかるはずです。デコレーターは、テーマ、モック、またはアプリケーション状態プロバイダーなどにも使用できます。個別のストーリーのStoryレベルまたは.rnstorybook/preview.tsxでグローバルにデコレーターを追加することもできます。
const meta = {
component: Input,
decorators: [
(Story) => (
<ThemeProvider>
<View style={{ padding: 16 }}>
<Story />
</View>
</ThemeProvider>
),
],
} satisfies Meta<typeof Input>;
ストーリーの書き方の詳細については、公式ドキュメントをご覧ください。
Storybookを共有する
Storybookはコンポーネントを分離して開発するのに優れていますが、もう一つの超能力は、チームメイトとコンポーネントを共有できることです。
先に進む前に、storybookを有効/無効にする方法をセットアップしましょう。EXPO_PUBLIC_ENVIRONMENTがstorybookに設定されている場合のみstorybookを表示するように_layoutファイルを編集します:
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開発者アカウントが必要です。これにより以下の機能が提供されます