小さなVueコンポーネントを作るときに気をつけるべきパターン with Storybook

# まえがき

VueでAtomicデザインの考え方を取り入れてAtomに相当する小さなコンポーネントを作っていくときに気をつけておきたいパターンです
Atomに相当する部分のコンポーネントは、コード量や複雑度は高くないですが最も多くの依存を受けるため
一度コンポーネントのインターフェイスが固まってしまうと、後から変更するのが難しい傾向にあります。
常に利用するときの意識を持ちながらコンポーネントを作るのが大事です

※ おまけでStorybookにカタログを作っていく際に気をつけることも書いています。

# Vueコンポーネント編

## パターン1 : emitされるeventの不足

### 問題

コンポーネントを利用する際にあってほしいイベントが受け取れない

### 例


-  Buttonなのにclickイベントが発行されない

```html
<template>
  <div>
    <!--このボタンのclickイベントは外からキャッチできない-->  
    <input type="button" value="button" /> 
  </div>
</template>
```

- Inputなのにchangeイベントしかなくinputイベントでリアルタイムな検証ができない


### 解決策

- 標準のDOMと違和感がないようなイベントをemitする

## パターン2：過剰なeventのemit

### 問題
mouseenter/mouseleaveなど大抵のDOMで利用できるイベントを、細かく自前でemitしなおす実装をしてしまう
デバッグツールに大量のイベントが表示されてしまいノイズになる

こういうコードを書いて

```vue
  <div @mouseleave="$emit('mouseleave')" >
  </div>
```
こうなる
![](https://qiita-image-store.s3.amazonaws.com/0/48729/c955950e-2126-b706-7b9e-a4659747a3e8.png)


### 解決策

`.native`や$listenersを使って必要なeventだけを自分でemitする
参考：[コンポーネントにネイティブイベントをバインディング](https://jp.vuejs.org/v2/guide/components-custom-events.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AB%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0)

## パターン3：過剰なprops依存

### 問題

propsが多すぎて、ユーザが使いにくいコンポーネントになる

### 例

- 表示・非表示をコントロールするだけのpropsを作る(外部からv-ifで切り替えるのと同じ)

```vue
<template>
  <div v-if="show">
  </div>
</template>

<script>
export default {
  name: "AppComponent",
  props: {
    show:Boolean
  },
}
```

### 解決策

- そのpropsが本当にいるのかをよく考える

上の例で言えば↓のように書けばshowはいらないはず

```vue
<app-component v-if="show" />
```

- [slot](https://jp.vuejs.org/v2/guide/components-slots.html)を使った設計に切り替えてpropsを絞る


## パターン4：propsの名前が直感的でない

### 問題

標準のHTMLやCSSで使われる単語を別の意味のpropsとして受け取れるようになっている
利用者は普段のHTMLと意味が乖離するので困惑する(バグを生みやすい)

### 例

- buttonのtypeが意味が違う(コンポーネントではデザインのためのpropsだが、標準のbuttonではsubmitなどのロールを担う）

```vue
<!-- コンポーネント名を見て、利用者はtypeに'submit'などの標準的なbuttonのattributeが入ることを期待しがち -->
<app-button type="submit" />
```

```vue:AppButton.vue
<template>
  <!-- 実際は全然関係ないところにtypeが使われている -->
  <div :class="type">
    <button></button>
  </div>
</template>

<script>
export default {
  name: "AppButton",
  props: {
    type:String
  },
};
</script>
```

### 解決策

- 常に標準のHTMLやCSSで利用される値を考慮して、概念と命名の感覚をなるべく揃える

## パターン5：レイアウトのためのCSSを内包している

### 問題

コンポーネントの外から与えたいレイアウトのためのCSS(margin、float、position、z-indexなど)がコンポーネント内部に内包されていて、利用が難しい

### 例

- z-indexが内包されていて外部からコントロールが難しい

```vue:Modal.vue
<template>
  <div><div class="modal"></div></div>
</template>
<style scoped>
.modal {
  z-index: 10000;
}
</style>
```

### 解決策

デザインのためのCSSとレイアウトのためのCSSを分けて考える。
レイアウトに使うCSSは「コンポーネントを利用する側」が与えられるような設計にする

```vue:App.vue
<template>
  <!-- 外からスタイルを与える -->
  <app-modal class="modal">
</template>
<style scoped>
.modal {
  z-index: 10000;
}
```

参考：http://apbcss.com/


## パターン6：data依存

### 問題

親に向けてemitすればいいだけの値を、コンポーネント自身のdataに一回保存してしまう

### 例
↓のAppInputコンポーネントでは初期値しか渡せませんが、初期値以外の値を渡そうとすると、propsの上書きの警告にぶつかることが多く、単純なコードでは実現できなくなることが多いです。

```vue:AppInput.vue
<template>
  <div><input type="text" v-model="value" /></div>
</template>

<script>
export default {
  name: "AppInput",
  props: {
    initValue: String
  },
  data() {
    return {
      innerValue: this.$props.initValue
    };
  },
  computed: {
    value: {
      get() {
        return this.innerValue;
      },
      set(value) {
        this.innerValue = value;
        this.$emit("input", value);
      }
    }
  }
};
</script>
```


### 解決策

dataの項目はなるべく減らす
上のAppInputコンポーネントの例では以下のようにすればそもそもdata自体が不要です

```vue:AppInput
<template>
  <!-- propsの値をそのまま子供に渡してしまう -->
  <div><input type="text" :value="value" @input="onInput" /></div>
</template>

<script>
export default {
  name: "AppInput",
  props: {
    value: String
  },
  methods: {
    onInput(event) {
      // 親へは純粋に値の橋渡しをするだけでいい
      this.$emit("input", event.target.value);
    }
  }
};
</script>
```

# Storybook編

storybookを作る目的はUIのカタログを作ると同時に、
**利用者が実装の詳細を追わなくてもstorybookを見ればコンポーネントを利用できる状態にする**ことにあるので、
それが実現されているかどうかを常に考える

## パターン１：propsとして渡せる値が分からない

### 問題

コンポーネントに渡せるプロパティとして何が用意されているのかわからない

### 解決策

[storybook-addon-vue-info](https://github.com/pocka/storybook-addon-vue-info)などを使って、コンポーネントのプロパティを出力する
![Storybook.png](https://qiita-image-store.s3.amazonaws.com/0/48729/a9f7fe07-3704-071e-87e6-c47f63dd1cb3.png)




## パターン２：propsの値のバリエーションが分からない

### 問題

`<button type="submit">`の`submit`のように、プロパティとして渡す値の選択肢が決まっているときに、それがStorybook上からわからない

### 解決策

- [knobを使う](https://www.npmjs.com/package/@storybook/addon-knobs)
    - 値のパターンが決まっているときはselectやradioなどを使う
    - 値のON/OFFのみならcheckboxを使う
    
![Storybook.png](https://qiita-image-store.s3.amazonaws.com/0/48729/b1296344-87f7-7063-bf87-2024ffb3a2ac.png)

- [storybook-addon-vue-info](https://github.com/pocka/storybook-addon-vue-info)を使ってプロパティの説明をちゃんと書く

## パターン3：emitされるイベントが管理されていない

### 問題

コンポーネントから発行される、（利用を想定している）イベントに対してactionが貼られていない・送られてくるpayloadがわからない

### 解決策

[action](https://www.npmjs.com/package/@storybook/addon-actions)を使って、想定される利用シーンを伝える
![Storybook.png](https://qiita-image-store.s3.amazonaws.com/0/48729/634c0882-fd86-3435-05d9-5c40de2c5ccf.png)