Storybookの具体的な活用例
開発者は品質の高いUIの開発と保守ができる
- コンポーネントのドキュメンテーション
- コンポーネントのテスト
- コンポーネントのデザインとスタイリングのプレビュー
- コンポーネントの再利用と共有
開発者はStorybookを使用してコンポーネントのドキュメンテーションを作成し、テストケースを作成し、リアルタイムでデザインやスタイリングの調整を行い、コンポーネントを再利用し共有することができます。
How to write stories
Component Story Format
The default export metadata controls how Storybook lists your stories and provides information used by addons. For example, here’s the default export for a story file Button.stories.js|ts:
default export : ストーリーファイル全体のメタデータを定義する
コンポーネントのタイトルや表示方法、プロパティの設定など
export const : 個々のストーリーを定義する
特定のコンポーネントの状態に対応するストーリーを記述します。異なるプロパティや引数を持つ複数のストーリーを作成することができる
Defining stories
import React from 'react';
import { Button } from './Button';
export const Primary = () => <Button state="primary" />;
この例では、Button
コンポーネントを<Button state="primary" />
としてレンダリングし、"Primary"という名前のストーリーとしてエクスポートしています。このようにして、異なるプロパティや状態を持つ複数のストーリーを定義することができます。
ストーリーファイル内でexport constを使用することで、Storybookはそれぞれのエクスポートを独立したストーリーとして認識します。これにより、複数のストーリーを定義してコンポーネントのさまざまな状態やプロパティを表示することができます。
Working with ReactHooks
React Hooksをストーリーの作成時に使用することは可能ですが、
ストーリーを作成する際は、可能な限りargsを使用することが推奨されている
import React, { useState } from 'react';
import { Button } from './Button';
export const ButtonWithHooks = () => {
const [state, setState] = useState('primary');
const handleClick = () => {
setState(state === 'primary' ? 'secondary' : 'primary');
};
return <Button state={state} onClick={handleClick} />;
};
↓
import React from 'react';
import { Button } from './Button';
export const ButtonWithHooks = ({ initialState }) => {
const [state, setState] = useState(initialState);
const handleClick = () => {
setState(state === 'primary' ? 'secondary' : 'primary');
};
return <Button state={state} onClick={handleClick} />;
};
ButtonWithHooks.args = {
initialState: 'primary',
};
この例では、ButtonWithHooks
コンポーネントの状態をinitialState
という引数で指定します。useState
フックを使って状態を管理し、handleClick
関数で状態の切り替えを行います。
ButtonWithHooks.args
を使用して、デフォルトの初期状態を'primary'
と設定します。これにより、Storybook上でこのコンポーネントを表示した際にデフォルトの初期状態が設定されます。
このように、argsを使用することでストーリーの作成が柔軟になり、異なる状態やプロパティの組み合わせを簡単に試すことができます。
Rename stories
特定のストーリーの名前を変更するには、該当するストーリーエクスポートの name
プロパティを変更します。以下は、"Primary" ストーリーの名前を変更する方法の例です:
import React from 'react';
import { Button } from './Button';
export const Primary = () => <Button state="primary" />;
Primary.storyName = '新しい名前のプライマリストーリー';
この例では、Primary
エクスポートの storyName
プロパティに新しい値を割り当てています。これにより、ストーリーブックのサイドバーに表示される名前が "新しい名前のプライマリストーリー" に変更されます。storyName
プロパティを変更することで、ストーリーに正確で説明的な名前を付けることができます。
How to write stories
A story is a function that describes how to render a component. You can have multiple stories per component, and the simplest way to create stories is to render a component with different arguments multiple times.
・.stories.tsxはどこのコンポーネントにおいてもいい
・コンポーネントごとに個別のストーリーを作成することができる(1つのファイル内に複数のストーリーを定義することが可能)
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
/*
*👇 レンダリング関数は、コンポーネントの表示方法を制御するためのフレームワーク固有の機能です。
* 詳細については、https://storybook.js.org/docs/react/api/csf
* を参照してレンダリング関数の使用方法を学びましょう。
*/
export const Primary: Story = {
render: () => <Button backgroundColor="#ff0" label="ボタン" />,
};
export const Secondary: Story = {
render: () => <Button backgroundColor="#ff0" label="😄👍😍💯" />,
};
export const Tertiary: Story = {
render: () => <Button backgroundColor="#ff0" label="📚📕📈🤓" />,
};
Using args
コンポーネントのストーリーに引数を導入して、改良する
ストーリーごとに作成および保守する必要がある定型コードが削減
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
backgroundColor: '#ff0',
label: 'Button',
},
};
export const Secondary: Story = {
args: {
...Primary.args,
label: '😄👍😍💯',
},
};
export const Tertiary: Story = {
args: {
...Primary.args,
label: '📚📕📈🤓',
},
};
argsをコンポーネントのストーリーに導入することのメリット
-
コードの量を減らすだけでなく、Primaryストーリーのargsを他のストーリーに展開することでデータの重複を減らすことができます。
-
他のコンポーネントのストーリーを書く際にargsをインポートして再利用することができます。
-
特にコンポジットコンポーネントを構築する際に役立ちます。例えば、ButtonGroupのストーリーを作成する場合、その子コンポーネントであるButtonのストーリーを組み合わせることができます。
Using the play function
コンポーネントのシナリオテストでは以下が用いられる
・@storybook/addon-interactions
・Storybookのplay関数
これらはレンダーごとに実行される小さなスニペットです
例えば、フォームコンポーネントのバリデーションを検証したい場合、次のようなストーリーを書くことができます。このストーリーでは、play関数を使用して入力欄に情報を入力した場合にコンポーネントがどのように応答するかを確認します。
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
component: LoginForm,
};
export default meta;
type Story = StoryObj<typeof LoginForm>;
export const EmptyForm: Story = {};
/*
* See https://storybook.js.org/docs/react/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// 👇 Simulate interactions with the component
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
// See https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'));
// 👇 Assert DOM structure
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!'
)
).toBeInTheDocument();
},
};
Using parameters
パラメータに関する要点を箇条書きでまとめます。
- パラメータはStorybookのストーリーに静的なメタデータを提供する方法です。
- ストーリーのパラメータは、ストーリーやストーリーグループのレベルで使用されます。
- パラメータを使用することで、ストーリーに構成情報を提供することができます。
- パラメータは、テーマやレスポンシブビューポートの設定など、ストーリーに関連するオプションを指定するために利用されます。
- パラメータはアドオンと組み合わせて使用することができ、ストーリーブックの柔軟性と拡張性を向上させます。
- アドオンとの統合により、ビジュアルやインタラクティブな側面をカスタマイズし、より豊かなテスト環境を構築することが可能です。
// Button.stories.ts|tsx
import type { Meta } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
//👇 Creates specific parameters for the story
parameters: {
backgrounds: {
values: [
{ name: 'red', value: '#f00' },
{ name: 'green', value: '#0f0' },
{ name: 'blue', value: '#00f' },
],
},
},
};
export default meta;
Using decorators
デコレータの使用
デコレータは、ストーリーをレンダリングする際に任意のマークアップでコンポーネントをラップする仕組みです。コンポーネントは、レンダリングされる「場所」に関する前提条件で作成されることが多いです。スタイルにはテーマやレイアウトのラッパーが必要な場合や、UIには特定のコンテキストやデータプロバイダが必要な場合などがあります。
簡単な例として、コンポーネントのストーリーにパディングを追加することを考えてみましょう。以下のように、ストーリーをdivでラップし、パディングを追加するデコレータを使用します。
// Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
{/* 👇 Storybookのデコレータは関数も受け付けます。 <Story/> を Story() で置き換えることで有効にします */}
<Story />
</div>
),
],
};
export default meta;
デコレータはより複雑なものにすることもでき、通常はアドオンによって提供されます。デコレータはストーリー、コンポーネント、グローバルレベルで設定することもできます。
Stories for two or more components
デザインシステムやコンポーネントライブラリを構築する際には、2つ以上のコンポーネントが連携して動作することがあります。例えば、親のListコンポーネントが子のListItemコンポーネントを必要とする場合などです。
この場合、各ストーリーに対して異なる関数をレンダリングすることが意味があります。
// List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
export const Empty: Story = {};
/*
*👇 レンダリング関数は、コンポーネントのレンダリング方法を制御するためのフレームワーク固有の機能です。
* 使用方法については、https://storybook.js.org/docs/react/api/csf
* を参照してください。
*/
export const OneItem: Story = {
render: (args) => (
<List {...args}>
<ListItem />
</List>
),
};
export const ManyItems: Story = {
render: (args) => (
<List {...args}>
<ListItem />
<ListItem />
<ListItem />
</List>
),
};
また、Listコンポーネントで子のListItemからストーリーを再利用することもできます。これにより、同じストーリー定義を複数の場所で更新する必要がなくなり、メンテナンスが容易になります。
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
//👇 We're importing the necessary stories from ListItem
import { Selected, Unselected } from './ListItem.stories';
const meta: Meta<typeof List> = {
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/react/api/csf
* to learn how to use render functions.
*/
export const ManyItems: Story = {
render: (args) => (
<List {...args}>
<ListItem {...Selected.args} />
<ListItem {...Unselected.args} />
<ListItem {...Unselected.args} />
</List>
),
};