OpenAIExpoSep 25, 2025, 5:45 PM

Building beautiful components faster with Storybook 9 and Expo

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

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

Building components faster with Storybook 9 and Expo

Key Points

  • Install with npm create storybook@latest (choose React Native)
  • Wrap Metro config with @storybook/react-native/metro/withStorybook
  • Gate the /storybook route with Stack.Protected or EXPO_PUBLIC_ENVIRONMENT

Practical guide to add Storybook 9 to an Expo Router app to build, test, and share React Native UI components in isolation. Prereqs: Expo app (SDK 50+), Expo Router, Node.js 20+, device/simulator. Install with npm create storybook@latest (choose recommended → React Native). Update metro.config.js by wrapping the default config with @storybook/react-native/metro/withStorybook. Add app/storybook.tsx that re-exports the .rnstorybook entry and gate the route in app/_layout.tsx using Stack.Protected (or process.env.EXPO_PUBLIC_ENVIRONMENT === 'storybook') so storybook is dev-only. Add a link to /storybook from your app index for quick access. Include your components directory in .rnstorybook/main.ts stories regex (e.g. '../components/**/.stories.') and restart Metro so storybook.requires.ts is regenerated. Write components and .stories.tsx files using args for default props and add decorators for padding, theme, or providers (per-story, file-level, or global in .rnstorybook/preview.tsx). Add an npm script like "EXPO_PUBLIC_ENVIRONMENT='storybook' expo start" to run storybook separately from normal app development. Sharing: you can build and distribute storybook-enabled app builds (iOS via TestFlight requires an Apple developer account).

Full Translation

Translations

A translation section that keeps the flow of the original article.

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 のリリースと公式ドキュメントを参照して、追加の機能や最新のベストプラクティスを確認してください。