Table上のリソースを定期ポーリングして入れ替えていくための自分なりの実装案 on Vue.js

## 前提

前回こんなことを書きました。

[vue.jsで、v-forしながらそのindexを利用してclassを切り替えたいような場合](https://qiita.com/keiichi-hikita/items/006e9ac0515014e9101b)

ここでは、

- リソースの一覧をTable表示し
- その上で各行に存在するリソースの操作メニューを別コンポーネントに切り出した

ということをやりました。

ここまでやるなら、リソース１つ１つ（table.tbody.trに相当)もComponentに切り出しちゃうほうが良いですよね。

で、そうしておくと、リソースのポーリングなんてのもすごくやりやすくなるなぁと思ったので、ちょっと書いてみます。

## 今回の記事の位置づけ

- 前回に加えてTableの各行に相当するリソースも別コンポーネントに切り出す
- その上でリソースの状態に応じてリソースを定期的にポーリングする
- ・・・という機能の実装案としてのメモ


例えば、

- OpenStackでインスタンスを作ったとします
- けどインスタンスがACTIVEになるまで、一覧上のリソースはぐるぐるリソースの状態をポーリングし続けますよね
- ではこれを実際に作るとどうなるかな

というような記事です。

## 分割していくとこうなった

結果的に、以下のような３つのコンポーネントに分かれました。
なお前回も書きましたが、私はBulma.jsを使ってますので、要素もそれを踏まえて読んでいただければ。

なお、今回は操作メニューについてのコードはあまり関係がないので省略します。

以下実際のコードです。
ポイントはコードの下に書きます。

### List.vue

Tableでリソースの一覧を表示するためのコンポーネントです。
Tableの定義をして、子コンポーネントを呼び出し、そこにAPIで取得したリソースを埋め込むのがお仕事です。


```html
<template>
  <span>
    <table class="table is-hoverable is-fullwidth">
      <thead>
        <tr>
          <th>{{ $t('ID') }}</th>
          <th>{{ $t('Name') }}</th>
          <th>{{ $t('Description') }}</th>
          <th>{{ $t('Status') }}</th>
          <th>{{ $t('') }}</th>
        </tr>
      </thead>
      <tbody>
        <list-element
          v-for="r in resources"
          :id="r.id"
          :key="r.id"
          :name="r.name"
          :description="r.description"
          :status="r.status"
          @resource-status-update="updateResource(r.id)"
        /> <!-- ポイント(1) -->
      </tbody>
    </table>
  </span>
</template>

<script>
import client from '~/clients'
import ListElement from './ListElement'

export default {
  components: {
    ListElement,
  },
  data() {
    return {
      resources: null,
    }
  },
  async mounted() {
    try {
      let res = await client.listResources()
      this.resources = res.data
    } catch (error) {
      (省略)
    }
  },
  methods: {
    updateResource: async function(pollingTargetID) { // ポイント(2)
      console.log(`Received polling request about ${pollingTargetID}`)
      let res = await client.getResource(pollingTargetID)
      let polledResource = res.data
      let copiedResources = [...this.resources]
      for (let i = 0; i < this.resources.length; i++) {
        if (this.resources[i].id == pollingTargetID) {
          copiedResources[i] = polledResource
        }
      }
      this.resources = copiedResources
    },
  },
}
</script>
```

### ListElement.vue

上の ``List.vue`` の ``tr`` に相当する部分です。
このコンポーネントをv-forしてリソースの一覧を記述します。

この子の仕事は、

- 親から与えられたリソース情報（プロパティ経由）を用いたtrタグのレンダ
- trに記載されるリソースがポーリングに値する状態であれば、ポーリング要求を親に（つまりList.vueに）飛ばす(※1)

です。

### 余談：直接子コンポーネントで書き換えちゃえばいいじゃない・・・と思いがち（自分も思ってた）

ここで※1についてですが、子コンポーネントでポーリングしてリソース情報を書き換えてしまえばいいのでは・・・と思いがちなのですが、親から与えられたプロパティを子コンポーネント側で書き込むのは非推奨なのですね。
以下のようなWarningが出てしまいます。

```
[Vue warn]: Avoid mutating a prop directly since the value
will be overwritten whenever the parent component re-renders.
Instead, use a data or computed property based on the prop's value.
Prop being mutated: "propsValue"
```

いろいろ記事はあると思いますが例えば https://stackoverflow.com/questions/40780730/vue-js-changing-props とか。
Vue.js自体のドキュメントにも記載があったはず。

ということで、親から変更を `かけてもらう` 必要があるわけです。

ということでコードコード。

```html
<template>
  <tr :class="{ 'is-status-update': isStatusUpdate }">
    <td>{{ id }}</td>
    <td>{{ name }}</td>
    <td>{{ description }}</td>
    <td>
      {{ status }}
      <progress v-if="isStatusUpdate" class="progress is-small is-link" max="100">
        15%
      </progress>
    </td>
    <td>
      <list-operation :id="id" :status="status" />
    </td>
  </tr>
</template>

<script>
import ListOperation from './ListOperation' // 前回説明したところ

export default {
  components: {
    ListOperation, // 前回説明したところ
  },
  props: {
    id: {
      type: String,
      default: '',
    },
    name: {
      type: String,
      default: '',
    },
    description: {
      type: String,
      default: '',
    },
    status: {
      type: String,
      default: '',
    },
  },
  data() {
    return {
      pendingStatus: ['PENDING_CREATE', 'PENDING_DELETE'] // 作成中、削除中みたいな意味
    }
  },
  computed: {
    isStatusUpdate: function() {
      return this.pendingStatus.includes(this.status)
    },
  },
  mounted() {
    const self = this
    // start polling request to parent component
    setInterval(function() { // ポイント(3)
      if (settings.pendingStatus.includes(self.status)) {
        console.log(`polling request has emitted about ${self.id}`)
        self.$emit('resource-status-update')
      }
    }, 2500) // 2500 msでポーリング
  },
}
</script>
```

## コードのポイント

### ポイント(1) @ 親コンポーネント

ここで子コンポーネントをv-forすることで要素をレンダしてます。
その際、リソースの情報をプロパティ経由で渡しています。
また、 `resource-status-update` というイベントに対するリスナを登録し、その際 `updateResource(r.id)` とすることで、該当のリソースのIDを利用して関数を呼び出しています。

### ポイント(2) @ 親コンポーネント

ここで `client.getResource(pollingTargetID)` を定義してます。

- この関数はリスナなので、子コンポーネントからの要求に基づき発火します
- pollingTargetIDはv-for内のkeyなので、リソースごとに異なります
- `client.getResoure` というのは、axios経由でリソースを取得するための関数です
- これが取得できたら、親コンポーネントのリソース一覧である、 `this.resources` の一部を入れ替えています

### ポイント(3) @ 子コンポーネント

- mounted(DOMがマウントされた時点)で、setIntervalを仕込みます
- setIntervalの中では、単にイベントだけを送出します（to 親コンポーネント)。（逆にそれ以外は何もしません）
- ただし、リソースのstatusが特定の状態であるときのみです。その特定の状態が、`this.pendingStatus` であり、実際には `['PENDING_CREATE', 'PENDING_DELETE']` という2つの状態です。

## つまりどういう動きになるの？

1. 親コンポーネントはリソースの一覧をaxios経由で取得します
2. これをv-forを使って、Table上に一覧化します。一覧のtrは子コンポーネントです
3. この子コンポーネントの１つのv-forループには、イベントリスナを登録しています。このリスナ経由で、子要素からの要求に基づき関数が実行されます。
4. その関数というのは、特定のリソースを取得するためのaxiosを利用した関数です
5. axios経由でリソースが取得できたら、上記1の時点でのリソースの一部を置き換えます
6. で、そもそも3で登録した関数というのはどうやると発火されるのかというと、子要素のmountedにて定義したsetIntervalでタイマーされてます
7. 上記の6は、リソースのstatusが特定の状態である間継続されます

とりあえずこんな感じで実装してうまく動いてます。

あとはstatusの状態に応じてtableのcssとかを切り替えたりして、それっぽく見せればいいのですが、それは見栄えの話なのでここでは省略します。

以上です。

自分の中でvue.jsでこの手のロジック実装する際どうするのがいいのかなぁ・・・と当初からの悩みだったので、比較的きれいに実装できてよかったかなと。（・・・と僕は勝手ながら思っている）