StorybookのReactのチュートリアルを見ながら基本を勉強してみた

React、Storybook初心者です。チュートリアルをやってみただけの投稿です。よろしくお願いします:bow:

私が実際に試したコードはこちらです。

https://github.com/okumurakengo/react-storybook-sample

# Storybook for React tutorial

[Get started | Storybook Tutorial](https://www.learnstorybook.com/react/en/get-started/)

### Setup React Storybook

```bash
# Reactのプロジェクト作成
npx create-react-app taskbox
cd taskbox

# Storybookを追加
npx -p @storybook/cli sb init

# port:9009 でコンポーネントエクスプローラーが起動する
yarn run storybook
```

<img width="1119" alt="Screen Shot 2019-09-29 at 16.19.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/bce2a196-f6f3-9ff6-166f-ef1e3345b410.png">

### Reuse CSS

1 チュートリアルを進める為に、cssをこちらかコピペする

↓このcssをコピーして
https://github.com/chromaui/learnstorybook-code/blob/master/src/index.css

`./src/index.css`にコピペする

2  `./.storybook/config.js` で `index.css` を読み込む

```js:.storybook/config.js
import { configure } from '@storybook/react';

// 追加
import '../src/index.css';

// automatically import all files ending in *.stories.js
configure(require.context('../src/stories', true, /\.stories\.js$/), module);
```

css読み込み前

<img width="858" alt="Screen Shot 2019-09-29 at 17.05.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/95f7a4ab-74c8-da17-384b-9d20682bd4c0.png">

css読み込み後

<img width="858" alt="Screen Shot 2019-09-29 at 17.05.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c378bcf3-4c41-8734-2f44-55fe268c933e.png">

### Add assets

フォントとアイコンも読み込む

↓ここのiconとfontフォルダをそのまま`./public/`フォルダに追加しておく
https://github.com/chromaui/learnstorybook-code/tree/master/public

# Build a simple component

[Build a simple component | Storybook Tutorial](https://www.learnstorybook.com/react/en/simple-component/)

### Task

タスクコンポーネントを作成する

### Get setup

`src/components/Task.js` と `src/components/Task.stories.js` を作成する

```jsx:src/components/Task.js
import React from 'react';

export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
  return (
    <div className="list-item">
      <input type="text" value={title} readOnly={true} />
    </div>
  );
}
```

```jsx:src/components/Task.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';

import Task from './Task';

export const task = {
  id: '1',
  title: 'Test Task',
  state: 'TASK_INBOX',
  updatedAt: new Date(2018, 0, 1, 9, 0),
};

export const actions = {
  onPinTask: action('onPinTask'),
  onArchiveTask: action('onArchiveTask'),
};

storiesOf('Task', module)
  .add('default', () => <Task task={task} {...actions} />)
  .add('pinned', () => <Task task={{ ...task, state: 'TASK_PINNED' }} {...actions} />)
  .add('archived', () => <Task task={{ ...task, state: 'TASK_ARCHIVED' }} {...actions} />);
```

Storybookはコンポーネントと、それのストーリーで構成されます。
コンポーネント1つに対して、必要な数のストーリーを作成できます。

- Component
    - Story
    - Story
    - Story

### Config

`./.storybook/config.js`を変更して作成した、stories.jsファイルを読み込めるようにします

```js:.storybook/config.js
import { configure } from '@storybook/react';
import '../src/index.css';

const req = require.context('../src', true, /\.stories.js$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);
```

このように表示されます。

<img width="857" alt="Screen Shot 2019-09-29 at 18.00.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/cefb1163-0d2a-92a4-7b45-2fac5324b1c3.png">


`storiesOf` とするたびにメニューが増え、
`add` とするたびに下の階層にメニューが増えてきます。

![Screen Shot 2019-09-29 at 18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/b6990c85-02b8-7b9a-a1a7-cd9f4ffedc37.png)

### Build out the states

storybookでコンポーネントを表示までできたので、
次にコンポーネントに状態やイベントを設定、UIの見た目を設定します。
storybookで見た目を確認しながら作成することができます。

```jsx:src/components/Task.js
import React from 'react';

export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
  return (
    <div className={`list-item ${state}`}>
      <label className="checkbox">
        <input
          type="checkbox"
          defaultChecked={state === 'TASK_ARCHIVED'}
          disabled={true}
          name="checked"
        />
        <span className="checkbox-custom" onClick={() => onArchiveTask(id)} />
      </label>
      <div className="title">
        <input type="text" value={title} readOnly={true} placeholder="Input title" />
      </div>

      <div className="actions" onClick={event => event.stopPropagation()}>
        {state !== 'TASK_ARCHIVED' && (
          <a onClick={() => onPinTask(id)}>
            <span className={`icon-star`} />
          </a>
        )}
      </div>
    </div>
  );
}
```

storybookでイベントを拾えたのを確認でき、状態ごとに見た目が変わることも確認できました。

![gCY4ipVxK2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/97aef7f3-4d4e-922d-7eaf-36ed0e50896a.gif)

### Specify data requirements

コンポーネントの見た目、イベントを作成完了まで行い、その後に`propTypes`を設定すると良いようです。

```jsx:src/components/Task.js
import React from 'react';
import PropTypes from 'prop-types';

export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
  return (
    <div className={`list-item ${state}`}>
      <label className="checkbox">
        <input
          type="checkbox"
          defaultChecked={state === 'TASK_ARCHIVED'}
          disabled={true}
          name="checked"
        />
        <span className="checkbox-custom" onClick={() => onArchiveTask(id)} />
      </label>
      <div className="title">
        <input type="text" value={title} readOnly={true} placeholder="Input title" />
      </div>

      <div className="actions" onClick={event => event.stopPropagation()}>
        {state !== 'TASK_ARCHIVED' && (
          <a onClick={() => onPinTask(id)}>
            <span className={`icon-star`} />
          </a>
        )}
      </div>
    </div>
  );
}

Task.propTypes = {
  task: PropTypes.shape({
    id: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    state: PropTypes.string.isRequired,
  }),
  onArchiveTask: PropTypes.func,
  onPinTask: PropTypes.func,
};
```

**※参考ページによると、この後にjestによるTask.jsのコンポーネントを書くという流れのようですが、今回はそこまでやりませんでした。**

# Assemble a composite component

[Assemble a composite component | Storybook Tutorial](https://www.learnstorybook.com/react/en/composite-component/)

コンポーネントを組み合わせた時の場合を見ていきます。

### Tasklist

Taskを組み合わせたTaskListをstorybookで表示します

### Get setup

`src/components/TaskList.js` と `src/components/TaskList.stories.js` を作成する

```jsx:src/components/TaskList.js
import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  if (loading) {
    return <div className="list-items">loading</div>;
  }

  if (tasks.length === 0) {
    return <div className="list-items">empty</div>;
  }

  return (
    <div className="list-items">
      {tasks.map(task => <Task key={task.id} task={task} {...events} />)}
    </div>
  );
}

export default TaskList;
```

```jsx:src/components/TaskList.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react';

import TaskList from './TaskList';
import { task, actions } from './Task.stories';

export const defaultTasks = [
  { ...task, id: '1', title: 'Task 1' },
  { ...task, id: '2', title: 'Task 2' },
  { ...task, id: '3', title: 'Task 3' },
  { ...task, id: '4', title: 'Task 4' },
  { ...task, id: '5', title: 'Task 5' },
  { ...task, id: '6', title: 'Task 6' },
];

export const withPinnedTasks = [
  ...defaultTasks.slice(0, 5),
  { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];

storiesOf('TaskList', module)
  .addDecorator(story => <div style={{ padding: '3rem' }}>{story()}</div>)
  .add('default', () => <TaskList tasks={defaultTasks} {...actions} />)
  .add('withPinnedTasks', () => <TaskList tasks={withPinnedTasks} {...actions} />)
  .add('loading', () => <TaskList loading tasks={[]} {...actions} />)
  .add('empty', () => <TaskList tasks={[]} {...actions} />);
```

![s2guCvO2VM.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/8c2ec751-8cba-af12-8a70-f477971c8c38.gif)

- デフォルト状態のリスト
- pinned状態のタスクありのリスト
- リスト読み込み中
- リストが0個

↑の状態でそれぞれstorybookで見れるとこまでできました。

### Build out the states

状態ごとのコンポーネントのUIを作り込んでいきます。

```jsx:src/components/TaskList.js
import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  const LoadingRow = (
    <div className="loading-item">
      <span className="glow-checkbox" />
      <span className="glow-text">
        <span>Loading</span> <span>cool</span> <span>state</span>
      </span>
    </div>
  );

  if (loading) {
    return (
      <div className="list-items">
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
      </div>
    );
  }

  if (tasks.length === 0) {
    return (
      <div className="list-items">
        <div className="wrapper-message">
          <span className="icon-check" />
          <div className="title-message">You have no tasks</div>
          <div className="subtitle-message">Sit back and relax</div>
        </div>
      </div>
    );
  }

  const tasksInOrder = [
    ...tasks.filter(t => t.state === 'TASK_PINNED'),
    ...tasks.filter(t => t.state !== 'TASK_PINNED'),
  ];

  return (
    <div className="list-items">
      {tasksInOrder.map(task => <Task key={task.id} task={task} {...events} />)}
    </div>
  );
}

export default TaskList;
```

今回はただコピペしただけですが、storybookでコンポーネントの表示を見ながら作成できるのでやりやすいと思いました。

![bqxVjYSAvl.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/93c09adc-d04a-40f9-c216-28bc28f66861.gif)

### Data requirements and props

コンポーネントの見た目、イベントを作成完了まで行ったので、`propTypes`も設定しておく

```jsx:src/components/TaskList.js
import React from 'react';

import Task from './Task';
import PropTypes from 'prop-types';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
    const events = {
        onPinTask,
        onArchiveTask,
    };

    const LoadingRow = (
        <div className="loading-item">
            <span className="glow-checkbox" />
            <span className="glow-text">
                <span>Loading</span> <span>cool</span> <span>state</span>
            </span>
        </div>
    );

    if (loading) {
        return (
            <div className="list-items">
                {LoadingRow}
                {LoadingRow}
                {LoadingRow}
                {LoadingRow}
                {LoadingRow}
                {LoadingRow}
            </div>
        );
    }

    if (tasks.length === 0) {
        return (
            <div className="list-items">
                <div className="wrapper-message">
                    <span className="icon-check" />
                    <div className="title-message">You have no tasks</div>
                    <div className="subtitle-message">Sit back and relax</div>
                </div>
            </div>
        );
    }

    const tasksInOrder = [
        ...tasks.filter(t => t.state === 'TASK_PINNED'),
        ...tasks.filter(t => t.state !== 'TASK_PINNED'),
    ];

    return (
        <div className="list-items">
            {tasksInOrder.map(task => <Task key={task.id} task={task} {...events} />)}
        </div>
    );
}

TaskList.propTypes = {
    loading: PropTypes.bool,
    tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
    onPinTask: PropTypes.func.isRequired,
    onArchiveTask: PropTypes.func.isRequired,
};

TaskList.defaultProps = {
    loading: false,
};

export default TaskList;
```

**※参考ページによると、この後にjestによるTaskList.jsのコンポーネントを書くという流れのようですが、今回はそこまでやりませんでした。**

---

最後まで読んでいただいてありがとうございました