摘要
本稿では、Next.js (TypeScript) プロジェクトに Storybook を導入し、設定する手順を示す。Storybook は UI コンポーネントの開発を支援するツールであり、効率的な開発を実現することができる。番外編ではatomicデザインのatomの部分を作成し、storybookで確認を行う。
プロジェクト の作成
まずは、プロジェクトを作成する。Next.js + TypeScript で開発を行うために、以下のコマンドを実行する。
npm create next-app --typescript <任意のプロジェクト名>
Storybook のインストール
まず、プロジェクトのディレクトリ内で、以下のコマンドを実行し、Storybook の最新版をインストールする。
npm install --save-dev @storybook/react@next
Storybook の設定
次に、以下のコマンドを実行し、Storybook の初期設定を行う。
npx sb@next init
このコマンド実行時に、「Do you want to run the 'eslintPlugin' fix on your project?」という質問が表示される場合がある。これは、Storybook の ESLint プラグインを導入するかどうかを問うものである。静的解析を行いたい場合は、"y" を入力し、"eslint-plugin-storybook" をインストールする。
ESLint の設定
その後、.eslintrc.json に以下の設定を追加することで、Storybook に関連する ESLint の設定を行う。
{
"extends": [
"plugin:storybook/recommended"
]
}
package.json の変更
上記手順に従って操作を行うと、package.json に以下のような変更が加わる。
Storybook の関連パッケージが devDependencies に追加される。
"scripts" セクションに、Storybook を起動するための "storybook" コマンドが追加される。
ディレクトリの変化
srcディレクトリの中にstoriesが作成される。rootディレクトリに.storybook/main.tsと.storybook/preview.tsが出来上がる。
番外編
自作したButtonをStorybookで確認
構築
Would you likeの部分はyes→yes→no→yes→no→enterの順で設定
$ npm create next-app --typescript atomic-demo
Need to install the following packages:
create-next-app@13.3.4
Ok to proceed? (y) y
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
? What import alias would you like configured? › @/*
・・・・・・・・・・・
・・・・・・・・・・・
uccess! Created atomic-demo
移動と確認
cdで作成したatomic-demoに移動して作業を行う。
$ cd atomic-demo
$ ls
README.md next.config.js package-lock.json public tsconfig.json
next-env.d.ts node_modules package.json src
ディレクトリを作成
$ pwd
<作成したところまでのpath>/atomic-demo
$ mkdir src/components
$ mkdir src/components/atoms
$ mkdir src/components/atoms/Button && touch src/components/atoms/Button/Button.tsx && touch src/components/atoms/Button/Button.module.scss && touch src/components/atoms/Button/Button.stories.tsx
#確認
$ ls src/components/atoms
Button.scss Button.stories.tsx Button.tsx
# ディレクトリ構成の確認(treeコマンドがある場合)
$ tree -I "node_modules"
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── next.svg
│ └── vercel.svg
├── src
│ ├── components
│ │ └── atoms
│ │ └── Button
│ │ ├── Button.module.scss
│ │ ├── Button.stories.tsx
│ │ └── Button.tsx
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── api
│ │ │ └── hello.ts
│ │ └── index.tsx
│ └── styles
│ ├── Home.module.css
│ └── globals.css
└── tsconfig.json
moduleのinstall
$ npm install --save-dev @storybook/react@next sass
storybook初期化
$ npx sb@next init
Need to install the following packages:
sb@7.0.8
Ok to proceed? (y) ← yを入力してenter
万が一以下の文が出て.storiesなどのディレクトリができない場合は以下のコマンドを実行
Detecting project type. ✓
There seems to be a Storybook already available in this project.
Apply following command to force:
sb init [options] -f
attention => Storybook now collects completely anonymous telemetry regarding usage.
This information is used to shape Storybook's roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://storybook.js.org/telemetry
実行コマンド
npx sb@next init -f
Button.tsxを作成
import React, {memo} from 'react';
import styles from './Button.module.scss';
export interface ButtonProps {
size?: 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge';
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
shape?: 'square' | 'rounded' | 'pill';
textAlign?: 'left' | 'center' | 'right';
onClick?: () => void;
text?: string;
}
// クラス名を生成する関数を追加
const getClassName = (
size: string,
color: string,
shape: string,
textAlign: string,
) => {
return [
styles[`btn-${size}`],
styles[`btn-${color}`],
styles[`btn-${shape}`],
styles[`text-${textAlign}`],
].join(' ');
};
const Button = memo((
{
size = 'medium',
color = 'primary',
shape = 'square',
textAlign = 'center',
onClick,
...props
}: ButtonProps) => {
return (
<button
// 生成されたクラス名を適用
className={getClassName(size, color, shape, textAlign)}
onClick={onClick}
>
{props.text}
</button>
);
});
export default Button;
eslintのエラーが出たら以下の方法で一旦応急処置
// eslint-disable-next-line react/display-name
const Button = memo((
Button.module.scss
// サイズ
.btn-small {
font-size: 12px;
padding: 4px 8px;
}
.btn-medium {
font-size: 16px;
padding: 6px 12px;
}
.btn-large {
font-size: 20px;
padding: 8px 16px;
}
.btn-xlarge {
font-size: 24px;
padding: 10px 20px;
}
.btn-xxlarge {
font-size: 28px;
padding: 12px 24px;
}
// 形状
.btn-square { border-radius: 0; }
.btn-rounded { border-radius: 4px; }
.btn-pill { border-radius: 9999px; }
// カラー
.btn-primary {
background-color: #007bff;
color: #fff;
&:hover { background-color: #0069d9; }
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
&:hover { background-color: #5a6268; }
}
.btn-success {
background-color: #28a745;
color: #fff;
&:hover { background-color: #218838; }
}
.btn-warning {
background-color: #ffc107;
color: #212529;
&:hover { background-color: #e0a800; }
}
.btn-danger {
background-color: #dc3545;
color: #fff;
&:hover { background-color: #c82333; }
}
// レスポンシブデザインのブレークポイント
$breakpoint-mobile: 480px;
$breakpoint-tablet: 768px;
// モバイルデバイス
@media (max-width: $breakpoint-mobile) {
.btn-small {
font-size: 10px;
padding: 3px 6px;
}
.btn-medium {
font-size: 14px;
padding: 5px 10px;
}
.btn-large {
font-size: 18px;
padding: 7px 14px;
}
.btn-xlarge {
font-size: 22px;
padding: 9px 18px;
}
.btn-xxlarge {
font-size: 26px;
padding: 11px 22px;
}
}
// タブレットデバイス
@media (min-width: $breakpoint-mobile + 1) and (max-width: $breakpoint-tablet) {
.btn-small {
font-size: 11px;
padding: 3.5px 7px;
}
.btn-medium {
font-size: 15px;
padding: 5.5px 11px;
}
.btn-large {
font-size: 19px;
padding: 7.5px 15px;
}
.btn-xlarge {
font-size: 23px;
padding: 9.5px 19px;
}
.btn-xxlarge {
font-size: 27px;
padding: 11.5px 23px;
}
}
// デスクトップデバイス
@media (min-width: $breakpoint-tablet + 1) {}
Button.stories.tsx
コードの説明は後ほど→ Button.stories.tsx の解説
import type {Meta, StoryObj} from '@storybook/react';
import Button from './Button';
const meta: Meta = {
title: 'atoms/Button',
component: Button,
argTypes: {
size: {
options: ['small', 'medium', 'large', 'xlarge', 'xxlarge'],
control: { type: 'radio' },
defaultValue: 'medium',
},
color: {
options: ['primary', 'secondary', 'success', 'warning', 'danger'],
control: { type: 'radio' },
defaultValue: 'primary',
},
shape: {
options: ['square', 'rounded', 'pill'],
control: { type: 'radio' },
defaultValue: 'square',
},
textAlign: {
options: ['left', 'center', 'right'],
control: { type: 'radio' },
defaultValue: 'center',
},
onClick: {
action: 'clicked',
description: 'Button clicked',
},
text: {
control: 'text',
defaultValue: 'Button',
},
},
};
export default meta;
type Button = StoryObj<typeof Button>;
export const Primary : Button = {
args : {
text: 'Button',
}
};
export const Secondary : Button = {
args : {
color: 'secondary',
size: 'xlarge',
shape: 'rounded',
textAlign: 'center',
text: 'Outline',
}
};
export const Danger : Button = {
args : {
color: 'danger',
size: 'large',
shape: 'rounded',
textAlign: 'center',
text: 'Danger',
}
};
export const Pill : Button = {
args : {
color: 'success',
size: 'xlarge',
shape: 'pill',
textAlign: 'center',
text: 'Pill Button',
}
}
.storybook/main.tsにstorybookに表示するためのpathを追記
storybookに表示するためのpathを追記 する。
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)",
"../src/components/**/*.stories.@(js|jsx|ts|tsx)", //追加した場所
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;
storybookを立ち上げて確認
$ pwd
<作成したところまでのpath>/atomic-demo
# Next.js立ち上げ
$ npm run dev
# StoryBook立ち上げ(別のターミナルで)
$ pwd
<作成したところまでのpath>/atomic-demo
$ npm run storybook
Button.stories.tsx の解説
// Storybook で使用する型をインポート
import type { Meta, StoryObj } from '@storybook/react';
// Button コンポーネントをインポート
import Button from './Button';
// ストーリーに関するメタ情報を定義
const meta: Meta = {
// ストーリーのタイトルを設定
title: 'atoms/Button',
// コンポーネントを指定
component: Button,
// 各プロパティの設定方法を定義
argTypes: {
// size プロパティの設定
size: {
// オプションを配列で指定
options: ['small', 'medium', 'large', 'xlarge', 'xxlarge'],
// ラジオボタンで選択できるように設定
control: { type: 'radio' },
// デフォルト値を設定
defaultValue: 'medium',
},
// color プロパティの設定
color: {
options: ['primary', 'secondary', 'success', 'warning', 'danger'],
control: { type: 'radio' },
defaultValue: 'primary',
},
// shape プロパティの設定
shape: {
options: ['square', 'rounded', 'pill'],
control: { type: 'radio' },
defaultValue: 'square',
},
// textAlign プロパティの設定
textAlign: {
options: ['left', 'center', 'right'],
control: { type: 'radio' },
defaultValue: 'center',
},
// onClick プロパティの設定
onClick: {
// アクションを定義
action: 'clicked',
// 説明を記載
description: 'Button clicked',
},
// text プロパティの設定
text: {
// テキスト入力で設定できるように設定
control: 'text',
// デフォルト値を設定
defaultValue: 'Button',
},
},
};
// メタ情報をエクスポート
export default meta;
// StoryObj 型を使用して Button の型を定義
type Button = StoryObj<typeof Button>;
// Primary ストーリーを定義
export const Primary: Button = {
// args を使用してプロパティを設定
args: {
text: 'Button',
},
};
// Secondary ストーリーを定義
export const Secondary: Button = {
args: {
color: 'secondary',
size: 'xlarge',
shape: 'rounded',
textAlign: 'center',
text: 'Outline',
},
};
// Danger ストーリーを定義
export const Danger: Button = {
args: {
color: 'danger',
size: 'large',
shape: 'rounded',
textAlign: 'center',
text: 'Danger',
},
};
// Pill ストーリーを定義
export const Pill: Button = {
args: {
color: 'success',
size: 'xlarge',
shape: 'pill',
textAlign: 'center',
text: 'Pill Button',
},
}
上記のコードでは、Primary、Secondary、Danger、および Pill という4つの異なるストーリーを定義している。それぞれのストーリーでは、args を使用して Button コンポーネントに渡すプロパティを設定している。これにより、Storybook 上で各ストーリーを表示した際に、指定したプロパティが適用された Button コンポーネントが表示される。
例えば、Danger ストーリーでは、color プロパティに 'danger'、size プロパティに 'large'、shape プロパティに 'rounded'、textAlign プロパティに 'center'、および text プロパティに 'Danger' が設定されている。これにより、Storybook 上で Danger ストーリーを選択すると、上記のプロパティが適用された Button コンポーネントが表示される。
最後に
以上で、Next.js (TypeScript) に Storybook を導入する手順を説明した。これにより、コンポーネントの開発が効率的に行えることが期待できるだろう。
参考文献
-
title : TypeScriptをベースにしたNext.jsのプロジェクトにStorybookのV7を使ってコンポーネント主導開発をやってみましょう
url : https://dev-yakuza.posstree.com/react/nextjs/storybook/v7/start/ -
title : フロントエンドエンジニアなら知らないと損!アトミックデザインのすゝめ
url : https://www.youtube.com/watch?v=Un0aoW0kNeE -
title : Atomic Design × Nuxt.js」コンポーネント毎に責務の範囲を明確にしたら幸せになった話 / GREE Tech Conference 2021
url : https://www.youtube.com/watch?v=DXlKK6aVBPk