
React Native for Web + TypeScriptを使ってStorybook公式のチュートリアルをやってみた
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Storybookとは、React、Vue、Angular 用の UI コンポーネントを開発・管理するためのオープンソースツールです。 公式ページにあるチュートリアルを、普段の業務で使用しているReact Native for WebとTypeScriptを使ってやってみたのでご紹介します。
実装するもの
- Storybook 公式ページのチュートリアルの タスク管理アプリ(当記事では、タスク一覧の表示までをご紹介します。)
今回作成したソースコードの一式はこちらにあります。
実装方針
- Create React App を使ってプロジェクトを作成する
- TypeScript を使用する
- 画面の作成に、React Native for Webのコンポーネントを使用する
- チュートリアルの内容(*2020/6/12 時点)に沿う *忠実に再現したものではなく、適時変更を加えている点をご了承ください。
プロジェクトの作成
Create React Appを使って React アプリを作成し、TypeScriptとReact Native for Webを導入します。今回は以下の記事でご紹介した方法で React アプリを作成し、TypeScript、React Native for Web、Prettierを導入しました。
* 以下、React Native for Web は RNfW と記載します。
Get Started
Storybook を導入する
以下のコマンドを実行し、Storybook を導入します。
$ npx -p @storybook/cli sb init
このコマンドによって.storybookフォルダ、src/storiesフォルダが自動で作成されます。
処理が終了したら、yarn storybookを実行し、Storybook が立ち上がるのを確認しましょう。

TypeScript、RNfW を実行するための設定を追加する
src/storiesフォルダ内に作られた story ファイルの拡張子を見ると.jsとなっています。こちらを.tsxとしても実行できるようにします。
.storybook/main.jsのstories: ['../src/**/*.stories.js']の箇所をstories: ['../src/**/*.stories.tsx']と変更しましょう。
module.exports = {
stories: ['../src/**/*.stories.tsx'],
....
} }
src/storiesフォルダ内の.stories.jsファイルを.stories.tsxと拡張子を変更し、yarn storybookで実行できることを確認します。
また、今回 RNfW を使用するため、.storybookフォルダ内に、webpack.config.jsを作成し、以下の通り記載しました。
module.exports = {
resolve: {
alias: {
'react-native$': 'react-native-web'
}
}}
*設定内容についてこちらの記事を参照させていただきました。
ここまでで、チュートリアルを始める準備は完了です。
Build a simple component
まず、タスク1件の情報を表示する Task コンポーネントを作成します。
このコンポーネントは「チェックボックス」「タスクタイトル」「お気に入りチェック用の ☆ マーク」で構成されています。
それぞのタスクはDefault(TASK_INBOX)/ Pinned(TASK_PINNED)/ Archived(TASK_ARCHIVED)の3つの状態を持ち、異なる UI を表示します。
src/components/Task.tsx
import React from 'react'
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'
const styles = StyleSheet.create({
container: {
height: 50,
borderWidth: 1,
borderColor: 'black',
justifyContent: 'center',
},
rowContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
checkAndTitle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
},
checkbox: {
marginLeft: 24,
fontSize: 24,
fontWeight: 'bold',
},
title: {
marginLeft: 24,
fontSize: 16,
fontWeight: 'bold',
},
star: {
marginRight: 24,
fontSize: 18,
},
})
export type TaskProps = {
task: {
id: string
title: string
state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED'
}
onArchiveTask: (id: string) => void
onPinTask: (id: string) => void
}
function Task(props: TaskProps) {
const {
task: { id, title, state },
onArchiveTask,
onPinTask,
} = props
return (
<View style={styles.container}>
<View style={styles.rowContainer}>
<View style={styles.checkAndTitle}>
<TouchableOpacity onPress={() => onArchiveTask(id)}>
<Text style={styles.checkbox}>
{state === 'TASK_ARCHIVED' ? '☑︎' : '□'}
</Text>
</TouchableOpacity>
<Text style={styles.title}>{title}</Text>
</View>
{state !== 'TASK_ARCHIVED' && (
<TouchableOpacity onPress={() => onPinTask(id)}>
<Text style={styles.star}>
{state === 'TASK_PINNED' ? '★' : '☆'}
</Text>
</TouchableOpacity>
)}
</View>
</View>
)
}
export default Task
続いて、Default(TASK_INBOX)/ Pinned(TASK_PINNED)/ Archived(TASK_ARCHIVED)それぞれの状態の UI を Storybook で確認できるよう story ファイルを作成します。
src/components/Task.stories.tsx
import React from 'react'
import { action } from '@storybook/addon-actions'
import Task from './Task'
export default {
component: Task,
title: 'Task',
excludeStories: /.*Data$/,
}
export const taskData: {
id: string
title: string
state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED'
} = {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
}
export const actionsData = {
onPinTask: action('onPinTask'),
onArchiveTask: action('onArchiveTask'),
}
export const Default = () => <Task task={{ ...taskData }} {...actionsData} />
export const Pinned = () => (
<Task task={{ ...taskData, state: 'TASK_PINNED' }} {...actionsData} />
)
export const Archived = () => (
<Task task={{ ...taskData, state: 'TASK_ARCHIVED' }} {...actionsData} />
)
yarn storybookを実行します。

「Default(チェック:なし、星:☆ を表示)」「Pinned(チェック:なし、星:★ を表示)」「Archived(チェック:あり、星:表示なし)」 と、それぞれの状態での UI が Storybook 上で確認できるようになりました。
Assemble a composite component
続いて、タスクを一覧表示するための TaskList コンポーネントを作成します。
src/components/TaskList.stories.tsx
import React from 'react'
import { View, Text } from 'react-native'
import Task from './Task'
export type TaskListProps = {
tasks: {
id: string
title: string
state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED'
}[]
onArchiveTask: (id: string) => void
onPinTask: (id: string) => void
}
function TaskList(props: TaskListProps) {
const { tasks, onArchiveTask, onPinTask } = props
if (tasks.length === 0) {
return (
<View>
<Text>You have no task.</Text>
</View>
)
}
return (
<View>
{tasks.map(task => (
<Task
key={task.id}
task={task}
onPinTask={onPinTask}
onArchiveTask={onArchiveTask}
/>
))}
</View>
)
}
export default TaskList
今回は「タスク6件全てが Default 状態の TaskList」「最後の1件のタスクが Pinned 状態の TaskList」「最後の1件のタスクが Archived 状態の TaskList」「タスクがない場合の TaskList」の それぞれの UI が確認できるように story ファイルを作成しました。
src/TaskList.stories.tsx
import React from 'react'
import TaskList from './TaskList'
import { taskData, actionsData } from './Task.stories'
export default {
component: TaskList,
title: 'TaskList',
decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
excludeStories: /.*Data$/,
}
export const defaultTasksData = [
{ ...taskData, id: '1', title: 'Task 1' },
{ ...taskData, id: '2', title: 'Task 2' },
{ ...taskData, id: '3', title: 'Task 3' },
{ ...taskData, id: '4', title: 'Task 4' },
{ ...taskData, id: '5', title: 'Task 5' },
{ ...taskData, id: '6', title: 'Task 6' },
]
export const withPinnedTasksData: {
id: string
title: string
state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED'
}[] = [
...defaultTasksData.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
]
export const withArchivedTasksData: {
id: string
title: string
state: 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED'
}[] = [
...defaultTasksData.slice(0, 5),
{ id: '6', title: 'Task 6 (archived)', state: 'TASK_ARCHIVED' },
]
export const Default = () => (
<TaskList tasks={defaultTasksData} {...actionsData} />
)
export const WithPinnedTasks = () => (
<TaskList tasks={withPinnedTasksData} {...actionsData} />
)
export const WithArchivedTasks = () => (
<TaskList tasks={withArchivedTasksData} {...actionsData} />
)
export const Empty = () => <TaskList tasks={[]} {...actionsData} />
再度 yarn storybookで、Storybook を起動します。

TaskList コンポーネントのそれぞれの状態での UI が確認できるようになりました。
まとめ
Storybook 公式のチュートリアルを React Native for Web + TypeScript で実装した内容をご紹介しました。
今回自身の入門としてチュートリアルに取り組みましたが、コンポーネントの UI を一覧形式で管理でき、それぞれの状態による変化も非常に把握しやすくなる点など、Storybook を導入するメリットを感じることができました。Addon の機能などについても、今後さらに調べていければと思います。
この記事がどなたかのお役に立てば幸いです。






