【Next.js】Storybookにふれる(1)
Storybookとは
Atomic Designなどの考え方に基づいてコンポーネント開発を行う場合、個々のコンポーネントの状態を確認するには、Pagesコンポーネントなどの画面表示用ファイルにコンポーネントを配置し、そのpropsに対して文脈に応じた値や関数を渡す必要があります。
こんな時にStorybookを使うことで、アプリケーション全体のコンテキストや他のコンポーネントの状態に依存することなく、デモデータを利用して個々のコンポーネントのUIテストを行うことができます。
開発が進むにつれてファイルが増え、管理が煩雑になりがちなコンポーネントの管理もStorybookを利用することで、視覚的、カタログ的に整理して管理できるようになります。
以下ではReact/Next.js環境でStorybookを導入する手順をまとめていますが、VueやAngularなどのフレームワークにも対応しているそうです。
実行環境
- Next.js: 14.2.5
- React: 18.0.0
- Styled-components: 6.1.12
- Storybook: 8.2.9
Storybookのインストールと起動
まずはプロジェクトディレクトリにて、以下コマンドを実行し、Storybookを起動するのに必要なパッケージをインストールします。
npx sb init
# 常に最新版をインストールするには以下
npx sb@latest init
Storybookを起動するためのスクリプトも自動でpackage.jsonに追加されるので、初期化後は以下コマンドでStorybookをすぐ起動できるようになります。
npm run storybook
起動が完了すると自動でブラウザの新しいページが開き、Storybookが表示されます。
自分が試したときはnpx sb init
のコマンドだけでブラウザ画面が開きました。
以下はStorybookブラウザの画面例です。
ここではStorybook初期化時にデフォルトで生成されるStorybookコンポーネント(Storyと呼ぶらしいので、本記事でも以下Storyと呼ばせていただきます)が表示されています。
コンポーネントを用意する
ここからはStorybookの画面上に表示させるコンポーネント(Story)を用意していきます。
Storybookにコンポーネントを表示させる場合、開発用コンポーネントファイルとは別に、Storybook用のファイルを用意する必要があります。
実際の開発ではすでに用意されているコンポーネントに対応するStoryを用意していく…という流れになるのかもしれませんが、今回は元となるコンポーネントファイルが存在しないので、まずは開発用コンポーネントの用意から始めていきます。
プロジェクトルートにcomponentsディレクトリを用意します。
そこにcomponents/StyledButton/index.tsx
というファイルを作ります。
このファイルではボタンコンポーネント用にスタイルを定義したstyled-componentsをexportしています。
このコンポーネントはpropsとして外部から値を渡せるようになっており、その値によって、ボタンの色が変わるようになっています。
import styled, { css } from "styled-components"
const variants = {
blue: {
color: '#ffffff',
backgroundColor: '#1D3461',
border: 'none',
},
green: {
color: '#ffffff',
backgroundColor: '#5AB203',
border: 'none',
},
transparent: {
color: '#111111',
backgroundColor: 'transparent',
border: '1px solid black',
},
} as const
type StyledButtonProps = {
variant: keyof typeof variants,
}
export const StyledButton = styled.button<StyledButtonProps>`
${({variant}) => {
const style = variants[variant]
return css``;
}}
border-radius: 12px;
font-size: 14px;
height: 38px;
line-height: 22px;
letter-spacing: 0;
cursor: pointer;
&:focus {
outline: none;
}
`
話が少し脱線してしまいますが、このサンプルコードで使用しているTypeScriptの記法について解説させてください。初見ではコードの構造を理解できなかった(今も難しいと感じています)ため、知識の定着を図るためにも、次に進む前に、このサンプルコード内で使用しているTypeScriptの記法について詳しく説明したいと思います。
脱線1:as const
上記のコードでvariants
というオブジェクトを定義していますが、その末尾にas const
をつけています。
このas const
は、TypeScriptに対して、このオブジェクトのプロパティや値をリテラル型として認識させるようにするためのキーワードです。
TypeScriptでは通常、オブジェクトのプロパティの値は広い型(stringやnumberなど)として推論されますが、as const
を付けることで、各プロパティの値が固定され、文字通りの値がそのまま型として扱われるようになります。
例えば、as const
をつけない状態でvariants
を定義し、後続のコードでvariants
の値を参照する場合、variants.blue.color
の型はstring
と推論されます。一方で、as const
をつけることで、variants.blue.color
の型はリテラル型である"#ffffff"
であるということが保証され、これにより、後続のコードで誤った値を使ってしまうというリスクを減らすことができます。
脱線2:typeofとkeyof
type StyledButtonProps = {
variant: keyof typeof variants,
}
この部分も一目見るだけでは動きがイメージしづらいです。。。
この、type StyledButtonProps
はStyledButtonコンポーネントに渡されるべきpropsの型定義をしていますが、その値であるvariant
に付与された型定義が少々複雑であります。
まずは typeof variants
の部分に注目してみます。
typeof
は通常のJavaScriptでも変数の型情報を参照する際に使用される演算子ですが、TypeScriptでは変数から型を抽出する際に使用されます。
今回は変数variants
から型を抽出するということですが、上記のas const
で見たとおりvariantsの型はリテラル型で保証されているため、抽出される型はvariants
にネストされたオブジェクトである blue: { ... }
かgreen: { ... }
かtransparent: { ... }
のいずれかということになります。
次に keyof
について見てみます。
keyofは以下のように説明されています。
オブジェクトの型からプロパティ名を型として返す型演算子です。
https://typescriptbook.jp/symbols-and-keywords#keyof-keyof型演算子-keyof-ts
typeof variants
によって保証される型は blue: { ... }
かgreen: { ... }
かtransparent: { ... }
のいずれかであるということはすでに確認しました。しかし、StyledButtonコンポーネントを別のコンポーネントから呼び出す実際の場面を考えてみますと、
(StyledButtonコンポーネントは以下のように記述します)
<StyledButton variant="blue">青いボタン</StyledButton>
<StyledButton variant="green">緑のボタン</StyledButton>
<StyledButton variant="transparent">透明なボタン</StyledButton>
variant
にはblue
かgreen
かtrasparent
のいずれかの情報さえ渡すことができれば良いので、blue: { ... }
かgreen: { ... }
かtransparent: { ... }
という情報はvariant
の型としては過剰な気がします。そのために、typeof variants
に対してさらにkeyof
をつけることで、「オブジェクトの型からプロパティ名を型として取得」することができ、StyledButtonコンポーネントに渡されるべきpropsの型を適切に定義できます。
ここまでの型定義によって、StyledButtonコンポーネントを呼び出す際は以下のように型チェックが可能になりました。
<StyledButton variant="blue">青いボタン</StyledButton> // OK
<StyledButton variant="green">緑のボタン</StyledButton> // OK
<StyledButton variant="pritty">かわいいボタン</StyledButton> // エラー: "pritty" は "blue" | "green" | "transparent” のいずれかの型に含まれないので「かわいいボタン」はNG
コンポーネントをStorybookに表示させるためのファイルを用意する
サンプルコードの構造についてわかってきたところで、引き続きこのコンポーネントをStorybook上で表示させるための設定を進めていきましょう。
npx sb init
で初期化すると、プロジェクトファイルにstoriesというディレクトリが追加されています。Storybook用のコンポーネントはこのディレクトリ配下にファイルを作成していきます。
ファイル名の命名規則としてはコンポーネント名.stories.tsx
というような形になります。
今回はStyledButton.stories.tsx
を作成します。
以下は先ほど定義したStyledButtonコンポーネントをStorybookのUI上に表示させるためのコード例となります。
propsにblue
, green
, transparent
それぞれの値を受け取るコンポーネントをそれぞれ個別にエクスポートしています。
import { Meta, StoryFn } from '@storybook/react'
import { StyledButton, StyledButtonProps } from '@/components/StyledButton'
export default {
title: 'StyledButton',
component: StyledButton
} as Meta<typeof StyledButton>
export const ButtonBlue: StoryFn<StyledButtonProps> = (props) => {
return (
<StyledButton {...props} variant="blue">
青いボタン
</StyledButton>
)
}
export const ButtonGreen: StoryFn<StyledButtonProps> = (props) => {
return (
<StyledButton {...props} variant="green">
緑のボタン
</StyledButton>
)
}
export const ButtonTransparent: StoryFn<StyledButtonProps> = (props) => {
return (
<StyledButton {...props} variant="transparent">
透明なボタン
</StyledButton>
)
}
以下詳細について解説していきます。
export default {...} as Meta の表現
この部分ではStoryのメタデータを定義しています。storybookからMeta型をインポートし、定義した内容をMeta型としてエクスポートすることで、Storybook側にStoryのメタデータとして渡すことができます。
先ほど、typeof variants
の表現について触れましたが、このMeta型にはコンポーネント自体の型(関数やクラスとしての型) が期待されているため、typeof StyledButton
を指定します。
メタデータ内部の定義に関しては、titleにはStorybook画面のメニュー部分に表示されるコンポーネントの名前を指定し、componentには表示させるコンポーネントのコンポーネント名を指定します。
メタデータには他にも設定を追加できるのですが、その詳細については後ほどご説明させていただきます。
StoryFn型
次に個々のStoryについて見ていきます。
ここでは、Storybook UI上に実際に表示されるコンポーネント(Story)を定義しています。Storyがpropsとして受け取る値の型はStoryFn
を使って参照することになります。
ここではStoryFn<StyledButtonProps>
という形components/StyledButton/index.tsx
で定義したStyledButtonProps
の型を参照しています。
(先ほどのcomponents/StyledButton/index.tsx
のサンプルコードでは、StyledButtonProps
の型定義にexport
の指定を忘れてしまっていました。このタイミングで追加しておきます!)
これで準備が整いましたので、Storybookのブラウザ画面を確認してみましょう。StyledButton.stories.tsx
でエクスポートした個々のStoryが表示されているはずです。
StorybookではCotnrolタブから画面に表示されたStoryに対してpropsの値を操作することができるのですが、この段階ではまだUI上からそれぞれのStoryを操作することはできません。
引き続き、Controlタブを使用可能にする設定を行なっていきます。
メタデータにargTypesを設定してStoryにpropsを渡せるようにする
Storyのメタデータを定義しているexport default { … }
の部分にargTypes
という設定を追加します。
export default {
title: 'StyledButton',
component: StyledButton,
argTypes: {
variant: {
control: { type: 'radio' },
options: ['blue', 'green', 'transparent'],
},
children: {
control: { type: 'text' },
},
},
} as Meta<typeof StyledButton>
argTypes
オプションではStorybookのControlタブからStoryに対して値を渡すための設定を行います。ここではStoryのvariantとchildrenに対して設定を行なっています。
contorolオプションではControlタブからどのような入力形式でpropsを渡すかということを定義しており、上記の場合だと、variantsの値に対してはラジオボタンによる選択形式、childrenに関してはテキスト入力形式で値を渡すような設定を行なっています。
またラジオボタンで値を入力する場合はその選択肢をoptionsで指定することができます。
childrenはボタン内部の文字列を意図しているのですが、この設定に関してひとつ注意点があります。
ここまでのサンプルコードではStyledButtonコンポーネント内部の文字列表示に関してはコンポーネントを呼び出す際に、StyledButtonタグで文字を直接ネストする形でボタン内部の文字列を表現していました。
しかし今回はpropsを経由してボタン内部の文字列の値を渡すため、StyledButtonPropsの型定義に型情報を追加する必要があります。
このままでは適切にStoryの設定ができないので、components/StyledButton/index.tsx
のtype StyledButtonProps
の部分を以下のように修正しておきます。
type StyledButtonProps = {
variant: keyof typeof variants,
children?: React.ReactNode,
}
これまで通りStyledButtonタグで直接文字をネストする形でコンポーネントを呼び出す可能性もあるのかなと思ったので、念の為children
には?
をつけておきます。(この部分最適な方法がわからなかったので引き続き確認してみたいと思います)
メタデータにargTypesのオプションが追加できたら、次にテンプレートとなるコンポーネントを定義します。
const Template: StoryFn<StyledButtonProps> = (args) => <StyledButton {...args} />
export const TemplateTest = Template.bind({})
TemplateコンポーネントではStyledButtonPropsの型通りにpropsを受け取り、受け取った値をスプレッド構文を使ってまとめてコンポーネントに渡しています。
そしてそのTemplateコンポーネントに対して.bind({})
を実行しエクスポートすることで、元の Template
コンポーネントをコピーする形で複数のStoryを作成することができます。
…失礼しました。
このままではpropsの値として何も渡せていない状態なので、propsの初期値の設定もしておきましょう。
Storyに対して初期値を設定するには以下のように定義します。
TemplateTest.args = {
variant: 'blue',
children: '青いボタン',
}
これでようやくStorybook UI上にブラウザから値を操作可能なコンポーネントを表示することができました。
初期表示時
緑のボタンにしてみます。
透明のボタンにも簡単に変更できます。
Button Blue、Button Green、Button Transparentの個別のStoryは一旦不要になるので、StyledButton.stories.tsx
からは削除しておきます。
ここまでの内容を以下コードにまとめます。
import { Meta, StoryFn } from '@storybook/react'
import { StyledButton, StyledButtonProps } from '@/components/StyledButton'
export default {
title: 'StyledButton',
component: StyledButton,
argTypes: {
variant: {
control: { type: 'radio' },
options: ['blue', 'green', 'transparent'],
},
children: {
control: { type: 'text' },
},
},
} as Meta<typeof StyledButton>
const Template: StoryFn<StyledButtonProps> = (args) => <StyledButton {...args} />
export const TemplateTest = Template.bind({})
TemplateTest.args = {
variant: 'blue',
children: '青いボタン',
}
以上が、Next.jsのコンポーネント開発においてStorybookを導入する手順です。
まだ触り始めたばかりなので、いろいろ試しながら操作に慣れていきたいですね。
StorybookではControlタブからコンポーネントに渡される値の操作だけでなく、Actionを使ったコールバックのハンドリングやコンポーネントのカタログ管理に役立つドキュメント作成の機能もあるので、それらの機能については次回触れてみたいと思います。
Discussion