Svelte で Presentational and Container Components コンポーネント設計をする

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/a250ed78-cbf6-5d7f-af45-ebcec924de5d.png" width=700 />

# はじめに

> **_`Svelte`にはスタイルガイドが存在しない。_**

`Svelte`を始めたいけど躊躇している方の中には、スタイルガイドやベストプラクティスが無いことも不安になる一因な気がしています。今回は`React`の設計手法として知られている`Presentational and Container Components`を`Svelte`に流用できないか試してみました。

結論

> `Presentational and Container Components`は`Svelte`でも導入メリットがあります。`Svelte`にはスタイルガイドやベストプラクティスがまだありませんが、`React`や`Vue`の設計手法を取り入れることが出来ます。

## 想定読者

- `Svelte`のベストプラクティスを集めいている方
- `Svelte`を触ってみたい方（[Githubにコードを上げている](https://github.com/Makohan/svelte-christmas-tree#svelte-christmas-tree)ので手元で確認できます）
- `Presentational and Container Components`というコンポーネント設計を知りたい方

# サンプルコードの紹介

## 環境

- "svelte": "^3.0.0",
- "typescript": "^3.9.3"

## こんなの作りました

アドベントカレンダーということで、[クリスマスツリーを飾り付けするゲーム](https://svelte-christmas-tree.vercel.app/
)を作りました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/1f6ab992-83ee-7d74-231b-dc90a52159ad.gif" width="200" />

# Presentational and Container Components コンポーネント設計

今回のフォルダ構成はBASE株式会社さんの資料「BASEにおける Vueコンポーネント設計の現在」[^1]を参考にさせていただきました。ありがとうございます！

資料をもとに作成したフォルダ構成です。（今は"ふ〜ん そうなんだ"くらいの気持ちで見てください。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/41488e3c-2d2c-3572-efda-8d163518c5fd.png)

## Presentational and Container Components とは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/0f6e5390-bbbc-e4c4-b80f-df62be4c6a58.png)


`Presentational and Container Components`とは`React`での設計手法で、一つのコンポーネントを見た目のコンポーネントとロジックのコンポーネントに分割します。
この設計手法のメリットは以下の通りです。

1. 見た目とロジックの責務を分離できる（関心の分離）
2. デザイナーの方がロジック処理を触ることなくスタイル調整できる
3. 見た目のコンポーネントの再利用性が向上する
4. アトミックデザインでの`layout`コンポーネントが作りやすくなる　※今回は言及しません

余談になりますが、この設計手法を明文化したDan Abramovさんは、今ではこの設計手法を推奨しておらず、ロジックの分離は`React`の`Custom Hooks`を使うことで同じ効果が得られる[^2]と言っています。`Svelte`には`Custom Hooks`のような`effect`（副作用）を制御しきれるものは無い認識なのでこちらは割愛いたします。

## 実装方法

以下の赤枠のコンポーネントが対象です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/150dccf4-8a53-102a-3383-407e5e35f816.png)
名前から想像がつくと思いますが

- ChristmasTree.svelte
  - `Container Components`（ロジック）です。
- ChristmasTreePresentation.svelte
  - `Presentational Components`（見た目）です。

### 処理の流れ

`Presentational and Container Components`思想のコンポーネントの処理の流れは`Container Components`でロジック処理（`store`へのアクセスや関数）を記述し、それらを`Presentational Components`に引き渡します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/4768ebfa-46bf-ca50-10e2-7c399d9f7156.png)

### 実際のコード

`Container Components`（ロジック）はこんな感じです。

```html:ChristmasTree.svelte（ロジック）
<script lang="ts">
  import ChristmasTreePresentation from "./ChristmasTreePresentation.svelte";
  import { ornament } from "../../store/ornament";
  import { treeOrnaments } from "../../store/treeOrnaments";
  ...（省略の意味。以下、同じ）

  // ベルのボタンのクリック処理
  function handleClickBell() {
    ornament.set({ type: "bell", colorHex: DEFAULT_BELL_COLOR });
  }

  // ベルボタンがアクティブか？
  let isActiveBell = false;

  // 飾り付けのstore
  let allOrnaments: TreeOrnament[] = [];
  treeOrnaments.subscribe((currentTreeOrnaments) => {
    allOrnaments = currentTreeOrnaments;
  });

  ...
</script>

<!-- Presentational Component に props を渡すだけ -->
<ChristmasTreePresentation
  {handleClickBell}
  {isActiveBell}
  {allOrnaments}
  ... />
```

一方、`Presentational Components`（見た目）はこんな感じです。

```html:ChristmasTreePresentation.svelte（見た目）
<script lang="ts">
  ...
  // Container Component からの props を受け取る
  // export let 〜 で宣言するとコンポーネントの引数になる。Svelte簡単!!
  export let handleClickBell: () => void;   // イベント処理
  export let isActiveBell: boolean;         // ボタンの非活性
  export let allOrnaments: TreeOrnament[];  // 全ての飾り付け
  ...
</script>

<!-- 見た目に関することだけ書いてあるのでHTML構造が分かりやすくなる -->
<Header />
<div class="mx-1 my-2">
  <!-- 受け取った関数や変数をセットするだけ -->
  <AppButton on:click={handleClickBell} color="red" isActive={isActiveBell}>
    <AppIcon id="bell" />
  </AppButton>
  ...
</div>
<div class="h-full rounded-lg bg-indigo-900 mx-2 mb-1">
  <AppCanvas on:click={(event) => handleClickCanvas(event)}>
    <!-- svelteはテンプレート構文 #each を使えます -->
    {#each allOrnaments as ornament (ornament.id)}
      <AppIcon ... />  // 省略してます
    {/each}
  </AppCanvas>
</div>
<Footer />
```

いかがでしょうか？
メリットである`「1. 見た目とロジックの責務を分離できる（関心の分離）」`が少しでも伝わっていたら嬉しいです。

## メリット

### メリット解説①　〜開発とデザイナーの実装箇所の分離〜

> 1.見た目とロジックの責務を分離できる（関心の分離）
> 2.デザイナーの方がロジック処理を触ることなくスタイル調整できる

`Presentational and Container Components`の設計手法を取り入れたことにより、**開発は`Container Component`を、デザイナーは`Presentational Component`を主に実装していく**ことになります。

実際の運用も考慮すると、変更対象ファイルがそのまま変更理由になるため、コードレビューもしやすくなると思われます。

たとえば、デザイナーの方が`Container Component`を変更していたら、誤ってロジックを変更していたり、見た目を変更したかったにも関わらず、`Container Component`を修正してしまっているかもしれません。逆もしかり。

### メリット解説② 〜コンポーネント管理のしやすさ〜

> 3.見た目のコンポーネントの再利用性が向上する

`Presentational and Container Components`には

- `Container Components`はステートフル（`store`にアクセスする等）
- `Presentational Components`はステートレス

という概念があります。

もしコンポーネントに`store`のロジック処理が書いてあった場合、コンポーネント設計の要である再利用性は著しく低下するでしょう。

`Storybook`を導入されている場合、コンポーネント管理もしずらくなるでしょうし、便利なアドオン `Knobs`[^3] などでは、動的に`props`を変更して見た目を確認やすくなるでしょう。~~（ と言いつつ、`Knobs` は `Svelte` 未対応でした:angel:）~~対応してました！

# 結びの言葉

`Svelte`にはスタイルガイドやベストプラクティスがまだありませんが、`React`や`Vue`の設計手法を取り入れることが出来ます。もし`Svelte`で開発していて解決策が見つからないときは、`React`や`Vue`でもググってみてください。きっと良い解決方法が見つかると思います。

また、私の作った**「クリスマスツリーを飾ろう」**を是非、触ってみてください！！
ちなみに、娘には3分で飽きられ、妻には「音は出ないの？ﾀﾞﾀﾞﾀﾞﾀﾞｯて飾れないの？こう〜（以下略）」と不満の声が絶えなかったゲームです。

昨日のアドベントカレンダーで弊社の爆速の貴公子@takaHALが[「Svelte + Vercel 爆速でサイトを公開する」](https://qiita.com/takaHAL/items/6632b58ffa3ad3168f97)と題打って記事を執筆しました。**「クリスマスツリーを飾ろう」**を`Vercel`にアップするきっかけにもなりました。ありがとさん！

**「クリスマスツリーを飾ろう」**
https://svelte-christmas-tree.vercel.app/

**Githubコードはこちら**
https://github.com/Makohan/svelte-christmas-tree

![svelte-christmas-tree.vercel.app_(iPhone 6_7_8 Plus).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/f9948a25-ad19-5ecf-fa5e-abf9a6bb1b01.png)

明日は@odn_chiくん、よろしく！

## 余談...

開発に着手して数時間後の様子がこちらです。

|  プレイ前  |  プレイ後  |
| ---- | ---- |
|  ![Screenshot_20201204_062530_com.android.chrome.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/8a42e31a-7d71-f4c1-21ef-f47675749a41.jpeg)  |  ![Screenshot_20201204_062818_com.android.chrome.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134221/e7eb54c9-61e2-c87c-4d5d-58de205a1983.jpeg)  |

_**「...これ、クリスマスツリーの飾り付けっていうか、ダンジョンメーカーじゃないか？！」**_

もう、バカです。バカバカバカ。

[^1]: [「BASEにおける Vueコンポーネント設計の現在」](https://speakerdeck.com/simezi9/baseniokeru-vuekonponentoshe-ji-falsexian-zai) @simezi9 さん。大変参考になりました。ありがとうございます！
[^2]: [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
[^3]: [Storybook Addon Knobs](https://github.com/storybookjs/storybook/tree/master/addons/knobs)