Storybook 9 と Expo でより速く美しいコンポーネントを構築する
Development • React Native • September 25, 2025 • 12 minutes read
ゲスト投稿: Daniel Williams — フロントエンド開発者兼オープンソースのメンテイナー。React/React Native の実務経験は10年以上。
このチュートリアルでは、Expo アプリに Storybook 9 を導入して、React Native の UI コンポーネントをより速く、摩擦なく構築・テスト・共有する方法を説明します。本記事は、最近リリースされた Storybook 9 に基づいており、これまでで最良の React Native サポートを提供します。
概要
複雑な画面を作っていると、成功/エラーなど多様な UI ステートが必要になります。コードの変更でフルリロードが発生すると、毎回アプリにログインして作業中の状態まで戻らなければならず、このワークフローはすぐに疲れてしまいます。もし各 UI ステートのカタログがあれば、変更前の状態に直接ジャンプでき、チームと簡単に共有してレビューしたり、CI 上で実行できる高速な自動テストに変換したりできます。
これこそが Storybook for React Native が目指すワークフローです。本記事では Expo アプリに Storybook をセットアップして最大限に活用する手順を案内します。
Expo アプリで Storybook 9 をセットアップする
Storybook はコンポーネントを孤立させて構築・テスト・ドキュメント化するための開発環境です。アプリ全体の複雑さに煩わされずに各コンポーネントを個別に作業できる「ワークショップ」のように考えてください。コンポーネントファーストのワークフローは視覚テスト、ライブドキュメント、そして大規模コードベースでのデザイン整合性維持に役立ちます。
前提条件
- 既存の Expo アプリ(Expo Router を使用)。SDK 50 以上を推奨
- 新しく始める場合は
npx create-expo-app で作成(新規作成後は npm run reset-project を実行してクリーンなプロジェクトに)
- Node.js 20+ と好みのパッケージマネージャ(bun, npm, yarn, pnpm)
- テスト用デバイスまたはシミュレータ
- 完成版のサンプルを確認したい場合は、example リポジトリの場所は元記事のリンクを参照してください。
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
その後、metro.config.js を Storybook サポートを含めるように更新します:
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 をアクセス可能にするようルートを設定します。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 ルートへ移動するためのリンクを追加します。まずは 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 ステートを表す実例です。たとえば Button コンポーネントの「Loading」「Disabled」といったストーリーを作れば、それぞれの状態を個別に確認できます。
以下では簡単な Input コンポーネントを作り、Storybook の機能を紹介します。
stories の検出設定に components ディレクトリを追加
Storybook が components フォルダ内のストーリーを拾うように、.rnstorybook/main.ts の stories 正規表現にパスを追加します:
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 ;
🚨 重要: stories の正規表現を変更したら Metro を再起動してください。これにより storybook.requires.ts ファイルが生成され、Storybook が新しいディレクトリの監視を有効にします。
シンプルな Input コンポーネントを作る
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 を作成します:
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" , } , } ;
これで Storybook 上に「First Name」ラベルと「John」プレースホルダが表示されるはずです。args はストーリーの初期 props を設定し、Controls パネルでリアルタイムに調整できます。
さらに別状態のストーリーを追加
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" , } , } ;
メニューに新しいステートが表示され、すばやく切り替えられるようになります。
デコレーターでラッパーを追加
Input コンポーネントが画面端にぴったり寄りすぎている場合、パディングを追加するラッパーが欲しくなります。そういったときにデコレーターが便利です。meta にデコレーターを追加します:
import { View } from "react-native" ;
const meta = {
component : Input ,
decorators : [
( Story ) => ( < View style = { { padding : 16 } } > < Story / > < / View > ) ,
] ,
} satisfies Meta < typeof Input > ;
これでコンポーネントの周囲に余白が追加されます。デコレーターはテーマ、モック、アプリケーションステートのプロバイダなどにも使えます。デコレーターはストーリー単位、ファイル単位、またはグローバルに .rnstorybook/preview.tsx で設定できます:
const meta = {
component : Input ,
decorators : [
( Story ) => (
< ThemeProvider >
< View style = { { padding : 16 } } >
< Story / >
< / View >
< / ThemeProvider >
) ,
] ,
} satisfies Meta < typeof Input > ;
詳細は公式ドキュメントを参照してください。
Storybook を共有する
Storybook はコンポーネントを孤立して開発するだけでなく、チームとコンポーネントを共有するのにも強力です。まずは Storybook の有効化/無効化方法を設定しましょう。
_layout ファイルを編集して、EXPO_PUBLIC_ENVIRONMENT が storybook に設定されているときだけ Storybook を表示するようにします:
// 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 >
)
}
次に package.json に environment をセットするスクリプトを追加します:
"storybook" : "EXPO_PUBLIC_ENVIRONMENT='storybook' expo start" ,
これで Storybook で開発したいときは npm run storybook を、通常のアプリ開発は npm run start を使い分けられます。環境が storybook でない場合はアプリ内の Storybook へのリンクを隠すなどの工夫もできます。
iOS での共有(TestFlight)
Apple の TestFlight を利用するには Apple Developer アカウントが必要です。これにより…
(元記事はここで文章が切れています。)
参考: Storybook 9 のリリースと公式ドキュメントを参照して、追加の機能や最新のベストプラクティスを確認してください。