OpenAIExpo2025/09/25 17:45

Building beautiful components faster with Storybook 9 and Expo

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

元記事

Quick Digest

要約

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

openaijamodel: gpt-5-mini-2025-08-07

Storybook 9 と Expo でコンポーネント開発を高速化

Key Points

  • Storybook 9でReact Native対応強化
  • Expo Routerへ簡単導入手順
  • storiesは再起動で反映(metro再起動必須)

要点:Storybook 9 を Expo アプリ(Expo Router)に組み込んで、React Native コンポーネントを隔離して作成・視覚テスト・共有・CI検証を速く行う手順をまとめます。 前提:Expo Router(SDK50+ 推奨)、Node.js 20+、デバイスまたはエミュレータ。 導入手順(実践): 1) インストール: npm create storybook@latest → 推奨設定 + React Native を選択(これで .rnstorybook と依存が追加される)。 2) Metro を設定: npx expo@latest customize metro.config.js し、metro.config.js に const withStorybook = require('@storybook/react-native/metro/withStorybook'); module.exports = withStorybook(config); を追加。 3) ルート作成: app/storybook.tsx を作成して export { default } from '../.rnstorybook'; を書く。app/_layout.tsx で Stack.Protected(DEV もしくは process.env.EXPO_PUBLIC_ENVIRONMENT === 'storybook')を使い storybook 画面を開発時または指定環境のみ表示。 4) stories 検出設定: .rnstorybook/main.ts に ../components/**/.stories. を追加し、変更後は Metro を再起動(storybook.requires.ts を再生成するため必須)。 5) コンポーネントと stories 作成: components/input.tsx と components/input.stories.tsx(Basic / Error / Disabled など)を作り、args と controls で状態を操作。デコレーターで余白・テーマ・プロバイダをラップ(story 個別・global の .rnstorybook/preview.tsx 両方可)。 6) 共有: package.json に "storybook": "EXPO_PUBLIC_ENVIRONMENT='storybook' expo start" を追加して npm run storybook で起動。iOS 共有は TestFlight 等を利用(Apple デベロッパー要)。 補足:Storybook 9 は React Native サポートが強化済み。コンポーネント単体でのデバッグ、視覚的ドキュメント化、CI 向けの軽量テストに有効です。

Full Translation

翻訳

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

openaijamodel: gpt-5-mini-2025-08-07

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

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 サポートを含めるように更新します:

// 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 をアクセス可能にするようルートを設定します。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.tsstories 正規表現にパスを追加します:

// .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 ;

🚨 重要: 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 を作成します:

// 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 パネルでリアルタイムに調整できます。

さらに別状態のストーリーを追加

ErrorDisabled のストーリーを追加してみましょう:

// 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" ; // add this
// components/input.stories.tsx
const meta = {
  component : Input ,
  decorators : [
    // ここでは 16px のパディングを持つコンテナを追加しています。
    ( 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_ENVIRONMENTstorybook に設定されているときだけ 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 のリリースと公式ドキュメントを参照して、追加の機能や最新のベストプラクティスを確認してください。