この記事は公開されてから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 の機能などについても、今後さらに調べていければと思います。
この記事がどなたかのお役に立てば幸いです。