この記事はフラー株式会社 Advent Calendar 2022の3日目の記事です。
前回は@chooblarinさんで、CSS Masks活用術でした。
はじめに
今年の夏ごろ、Storybook v7.0.0のalpha版がリリースされて、だんだんv7.0.0の正式リリースが現実味を帯びてきたように思います。
先日、Storybookのv7.0.0用のドキュメントを偶然見つけたのですが、サンプルコードが新しいフォーマットで書いてありました。
前々から新しい書き方が出ることは知っていましたが、v7.0.0からそれがデフォルトになりそうな気配を感じたので、このタイミングで改めて見ていこうと思います。
動作環境
以下に出てくるサンプルコードは以下のような環境で動かしています。
- Vite: v3.2.4
- TypeScript: v4.9.3
- React: v18.2.0
- @storybook/react: v6.5.3
CSF3.0とは?
CSF(Component Story Format)3.0 は新しいStoryのフォーマットです。
(これを知るまで、今まで書いていたのが CSF 2.0と知りませんでした......。)
従来の書き方よりシンプルに書けるのが特徴です。
公式のブログでは、2021年7月に紹介されていました。
フラグを有効にしていればv6.3系から、フラグを有効にしていなくてもv6.4系からCSF 3.0のフォーマットでStoryが書けるようになっています。
(そのため、仕事でちょくちょく使ってはいます。)
書き方の違いを見てみる
ここからは、CSF 3.0での書き方と、以前のCSF 2.0での書き方の違いを見ていこうと思います。
サンプルにするのは以下のようなボタンのコンポーネントです。
import { ReactNode } from "react";
import classes from "./Button.module.css";
type ButtonProps = {
children: ReactNode;
bgColor?: "white" | "black";
};
export const Button = ({ children, bgColor = "white" }: ButtonProps) => {
return (
<button
className={`${classes.button} ${
bgColor === "black" ? classes.blackBg : ""
}`}
>
{children}
</button>
);
};
.button {
padding: 4px 8px;
font-size: 14px;
color: black;
background: white;
border-radius: 16px;
}
.blackBg {
color: white;
background: black;
}
そして、CSF 2.0の書き方でStoryを作っていくと、こんな感じになるはずです。
import { ComponentMeta, ComponentStoryFn } from "@storybook/react";
import { Button } from "./Button";
export default {
title: "components/Button",
component: Button,
} as ComponentMeta<typeof Button>;
export const Primary: ComponentStoryFn<typeof Button> = (args) => (
<Button {...args} />
);
これをCSF 3.0の書き方で書くとこんな感じになります。
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { Button } from "./Button";
export default {
component: Button,
} as ComponentMeta<typeof Button>;
export const Primary: ComponentStoryObj<typeof Button> = {};
この例だとパッと違いが出てきませんが、
- 名前付きexportでexportされるものが関数ではなくオブジェクトになっている
- デフォルトexportで書くtitleを省略できる(Storybook v6.4.0-)
といった点が異なります。
もっと違いが出てくるのは、Storyを複数作るときです。
CSF 2.0で、同じコンポーネントで複数のStoryを書くとき、このように書くと思います。
export const Primary: ComponentStoryFn<typeof Button> = (args) => (
<Button {...args} />
);
Primary.args = { children: "sample", bgColor: "white" };
Primary.decorators = [
(Story) => (
<div style={{ padding: "20px" }}>
<Story />
</div>
),
];
export const PrimaryBlack = Primary.bind({});
PrimaryBlack.args = { ...Primary.args, bgColor: "black" };
PrimaryBlack.decorators = Primary.decorators;
例をわかりやすくするためにdecoratorsも設定してあげています。
同じようなことをCSF 3.0の書き方でやろうとすると、こうなります。
export const Primary: ComponentStoryObj<typeof Button> = {};
export const PrimaryBlack = {
...Primary,
args: { ...Primary.args, bgColor: "black" },
};
CSF 2.0ではbindでPrimaryをコピーしつつ、args,decoratorsを設定しなおす必要がありました。
対してCSF 3.0ではPrimaryをスプレッド構文で展開しつつ、変えたいところだけを設定しなおすことができるようになっています。
このようにCSF 3.0ではexportするのがオブジェクトになったため、メタデータをコピーしやすくなっています。
こんな感じでいい感じに書けるCSF 3.0ですが、この書き方だと困る点が1つあります。
それは、StoryにReact Hooksを使う場合です。
例えばこんな感じのCheckBoxのコンポーネントがあるとします。
type CheckBoxProps = {
checked: boolean;
onClick: () => void;
};
export const CheckBox = ({ checked, onClick }: CheckBoxProps) => {
return <input type="checkbox" checked={checked} onClick={onClick} />;
};
CSF 2.0だと、React Hooks を使って、こんな感じに書くことができます。
export const Primary: ComponentStoryFn<typeof CheckBox> = () => {
const [checked, setChecked] = useState(false);
return <CheckBox checked={checked} onClick={() => setChecked(!checked)} />;
};
これの何がうれしいかというと、CheckBoxの挙動を疑似的にStorybook上で再現できるので、Storybook上での挙動の確認がしやすいという点です。
(もちろんReact Hooksを使わずに書いても、この例の場合はStorybookのControlsでargsを触ることができるので特に支障はないと思います。)
今までのCSF 3.0の例だとオブジェクトをexportする形なので、React Hooksを使ってカスタマイズしたコンポーネントを表示させる手段がありません。
そこで出てくるのが、renderというメソッドです。
オブジェクトにrenderというメソッドを定義して要素を返す関数を渡すことで、上の例でやったことを同じように実現することができます。
具体的にはこんな感じです。
export const Primary: ComponentStoryObj<typeof CheckBox> = {
render: () => {
const [checked, setChecked] = useState(false);
return <CheckBox checked={checked} onClick={() => setChecked(!checked)} />;
},
};
以上のように、大体今までの書き方でできたことは、CSF 3.0でも同じように書けます。
おわりに
CSF 2.0とCSF 3.0の書き方を見てきました。
CSF 3.0は他にも「Play functions」という機能があり、レンダリング後にコードの実行ができるようです。
そちらはまだ試していないので、後日触ってみようと思います。
明日は @furusax さん で「元アルバイトの彼に向けたポエム」です。
参考
https://storybook.js.org/blog/component-story-format-3-0/
https://storybook.js.org/docs/7.0/react/get-started/introduction
https://storybook.js.org/blog/writing-stories-in-typescript/