Storybook7探訪
The above story definition can be further improved to take advantage of Storybook’s “args” concept. Args describes the arguments to Button in a machine-readable way. It unlocks Storybook’s superpower of altering and composing arguments dynamically.
argsを使うことが推奨されている
props無い場合はこれでもいい?
export default {
title: 'Button',
component: Button,
};
export const Primary = {};
キーボードショートカットもある
The “Docs” page displays auto-generated documentation for components (inferred from the source code). Usage documentation is helpful when sharing reusable components with your team, for example, in an application.
これすごいよなー
Provider系はpreview.jsでdecoratorとして書く
// .storybook/preview.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
export const decorators = [(Story) => <ThemeProvider theme="default">{Story()}</ThemeProvider>];
headでCSSなどを読み込んでいる場合
Alternatively, if you want to inject a CSS link tag to the <head> directly (or some other resource like a webfont link), you can use .storybook/preview-head.html to add arbitrary HTML.
argsを使えというのはこの書き方をさせたいかららしい
// Button.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'Button',
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
backgroundColor: '#ff0',
label: 'Button',
};
export const Secondary: Story = {
args: {
...Primary.args,
label: '😄👍😍💯',
},
};
export const Tertiary: Story = {
args: {
...Primary.args,
label: '📚📕📈🤓',
},
};
List系のComponentのStoryを作るときに、単体のStoryを再利用しろと
// ButtonGroup.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ButtonGroup } from '../ButtonGroup';
//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';
const meta: Meta<typeof ButtonGroup> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'ButtonGroup',
component: ButtonGroup,
};
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
export const Pair: Story = {
args: {
buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
orientation: 'horizontal',
},
};
When Button’s signature changes, you only need to change Button’s stories to reflect the new schema, and ButtonGroup’s stories will automatically be updated. This pattern allows you to reuse your data definitions across the component hierarchy, making your stories more maintainable.
たしかに。全体として「再利用」が鍵っぽい。メンテナビリティ意識。
dar/light mode設定はpreview.jsで
// preview.js
// All stories expect a theme arg
export const argTypes = { theme: { control: 'select', options: ['light', 'dark'] } };
// The default value of the theme arg to all stories
export const args = { theme: 'light' };
URLからargsを読み取る
For example, args=obj.key:val;arr[0]:one;arr[1]:two;nil:!null will be interpreted as:
{
obj: { key: 'val' },
arr: ['one', 'two'],
nil: null
}
Parameterの使い方
Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons.
For example, let’s customize the backgrounds addon via a parameter. We’ll use parameters.backgrounds to define which backgrounds appear in the backgrounds toolbar when a story is selected.
Global parameterはやはりpreview.jsで
// .storybook/preview.js
export const parameters = {
backgrounds: {
values: [
{ name: 'red', value: '#f00' },
{ name: 'green', value: '#0f0' },
],
},
};
Layoutもdecoratorに書くと良さそう
// YourComponent.stories.js|jsx
import React from 'react';
import { YourComponent } from './YourComponent';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'YourComponent',
component: YourComponent,
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
<Story />
</div>
),
],
};
イメージ:Topページ
const meta: Meta<typeof HomePage> = {
component: HomePage,
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<RootLayout>
<Story />
</RootLayout>
),
],
}
export default meta
type Story = StoryObj<typeof HomePage>
export const Default: Story = {}
レイアウトごとにテンプレート組むと良いのかも
RootLayoutはpreview.jsにすればよいか
いや、/component/以下ではRootLayout欲しくないのでやはりページごとに書くのが良さそう
気になってたやつ
play関数の再利用
// MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'MyComponent',
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/*
* See https://storybook.js.org/docs/7.0/react/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FirstStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
userEvent.type(canvas.getByTestId('an-element'), 'example-value');
},
};
export const SecondStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('other-element'), 'another value');
},
};
export const CombinedStories: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Runs the FirstStory and Second story play function before running this story's play function
await FirstStory.play({ canvasElement });
await SecondStory.play({ canvasElement });
await userEvent.type(canvas.getByTestId('another-element'), 'random value');
},
};
fireEventとuseEventはどう違うんだろう。これはtesting-libraryの知識?
// MyComponent.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fireEvent, userEvent, within } from '@storybook/testing-library';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'ClickExamples',
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* 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 ClickExample: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// See https://storybook.js.org/docs/7.0/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'));
},
};
export const FireEventExample: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// See https://storybook.js.org/docs/7.0/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await fireEvent.click(canvas.getByTestId('data-testid'));
},
};
テストの再利用についてはこれが詳しい。Storyの記述をそのままtestに使える。
play関数のfactory記述、使い所ありそう
// index.stories.tsx
import type { ComponentStoryObj } from "@storybook/react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { UserCreateForm } from "./";
type Story = ComponentStoryObj<typeof UserCreateForm>;
export default { component: UserCreateForm };
const type = (step: 0 | 1 | 2 | 3) => {
if (step === 0) return;
userEvent.type(screen.getByPlaceholderText("姓"), "田中");
if (step === 1) return;
userEvent.type(screen.getByPlaceholderText("名"), "太郎");
if (step === 2) return;
userEvent.type(
screen.getByPlaceholderText("メールアドレス"),
"example@gmail.com"
);
};
const playFactory = (step: 0 | 1 | 2 | 3) => async () => {
type(step);
userEvent.click(screen.getByRole("button"));
};
export const Invalid1: Story = {
storyName: "未入力で送信",
play: playFactory(0),
};
export const Invalid2: Story = {
storyName: "姓未入力で送信",
play: playFactory(1),
};
export const Invalid3: Story = {
storyName: "名未入力で送信",
play: playFactory(2),
};
export const Valid: Story = {
storyName: "正常入力で送信",
args: { handleSubmit: (data) => {} },
play: playFactory(3),
};
引用元
Chromatic上でVRTの結果にコメントを残すことができるらしい。これならプロトタイピング時などのスピード出したいときにデザインエンジニアがFigmaとおさらばできるかも。
Loader. Experimentalらしい
使い方
// TodoItem.stories.js|jsx|ts|tsx
import React from 'react';
import fetch from 'node-fetch';
import { TodoItem } from './TodoItem';
export default {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'Examples/Loader'
component: TodoItem,
render: (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />,
};
export const Primary = {
loaders: [
async () => ({
todo: await (
await fetch('https://jsonplaceholder.typicode.com/todos/1')
).json(),
}),
],
};
いつ使いたくなるだろう
他の機能でも需要は満たせる気がする
// .storybook/preview.js
import fetch from 'node-fetch';
export const loaders = [
async () => ({
currentUser: await (await fetch('https://jsonplaceholder.typicode.com/users/1')).json(),
}),
];
左側バーのコンポーネント表示順はソートできたりするらしい
Pageのような大きくて複雑なコンポーネントのStoryを書く際の指針。
以前まではPresentation層書いておけば良かったけど、Suspence活用しようと思ったらデータフェッチは各地で行われる設計になるから純粋にPresentationalなPageを書くのは無理筋になった。こういうときはどうすればいいのだろうか?
Answer:ネットワークリクエストをモックしてください
If you need to render a connected component in Storybook, you can mock the network requests to fetch its data. There are various layers in which you can do that.
モック方法
- Provider → decorator
- Web API → MSW addon
ほとんどMSWでやる想定
なんか重要なことが書いてある気がするんだけど例を見てもpainがよくわからんかった
Avoiding mocking dependencies
It's possible to avoid mocking the dependencies of connected "container" components entirely by passing them around via props or React context. However, it requires a strict split of the container and presentational component logic. For example, if you have a component responsible for data fetching logic and rendering DOM, it will need to be mocked as previously described.It’s common to import and embed container components amongst presentational components. However, as we discovered earlier, we’ll likely have to mock their dependencies or the imports to render them within Storybook.
Not only can this quickly grow to become a tedious task, but it’s also challenging to mock container components that use local states. So, instead of importing containers directly, a solution to this problem is to create a React context that provides the container components. It allows you to freely embed container components as usual, at any level in the component hierarchy without worrying about subsequently mocking their dependencies; since we can swap out the containers themselves with their mocked presentational counterpart.
複数のコンポーネントStoryを記述する際にTemplateを作成する
// List.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import { List } from './List';
import { ListItem } from './ListItem';
//👇 Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';
const meta: Meta<typeof List> = {
/* 👇 The title prop is optional.
* Seehttps://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'List',
component: List,
};
export default meta;
type Story = StoryObj<typeof List>;
//👇 The ListTemplate construct will be spread to the existing stories.
const ListTemplate: Story = {
render: ({ items, ...args }) => {
return (
<List>
{items.map((item) => (
<ListItem {...item} />
))}
</List>
);
},
};
export const Empty = {
...ListTemplate,
args: {
items: [],
},
};
export const OneItem = {
...ListTemplate,
args: {
items: [{ ...Unchecked.args }],
},
};
テストについて詳しく見る前に今後の動向について
Our builder ecosystem continues to evolve. One of the biggest new entrants into the space is Webpack’s successor, Turbopack. We’re partnering with the Turbopack team to add support in 2023. In parallel, we’re continuing to invest in Storybook’s Vite support, which improved by leaps and bounds in 2022.
Viteに加えてTurbopackも
Next.jsへのサポート手厚いし順当な流れか?
Accessibility testing
Storybook already supports automated accessibility testing, both as an interactive addon and through automated tests. In 2023, we plan to unify these two approaches to continuously audit component accessibility.
Storybookを追いかけると自ずとa11yの勉強になりそうなのがいいね
Full page testing
Storybook is known as a tool for isolated component development and design systems, but it can also be used to develop connected components and even full-stack applications. We’re investing more in these scenarios from a testing standpoint. This gives devs a powerful way to exercise full application user flows but with the speed & reliability of Storybook’s isolated environment.
気になるところ。
Learning & docs overhaul
With more features than ever, it’s tricky for users to understand how to best use Storybook. We’re investing in more learning content like API docs and code snippets, as well as infrastructure to help automatically keep everything up to date.
がんばってほしい。ぶっちゃけドキュメント心もとなかった
応援してます!