【Vue.js】"空白の画面"を回避するために裏でAPIを実行して古いデータとひっそり差し替える

# やりたいこと
Vue.js にて、画面に表示するデータを API で取得する場合、API のレスポンスが返ってくるまでの間、画面には何も表示されない時間ができてしまいます。

API のレスポンスが返ってくるまでの一瞬なので、大きな問題ではありませんが、スムーズな画面表示を装うために、できるだけこの何も表示されない時間を作りたくありませんでした。

そこで、コンポーネントの初回表示時に、API で取得したデータをキャッシュし、
次回以降、同じコンポーネントを再表示する時には、初回表示時にキャッシュしたデータを表示しつつ、裏で新しいデータを取得し、こっそりデータを置き換えるようにすることで、二回目以降の画面表示では、この何も表示されない時間を無くすようにしました。

## くわしく
たとえば、以下のような 現在の天気一覧を表示する画面 があるとします。
<img width="250px" alt="サンプルのリスト" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/98db19b7-57b1-c1aa-defb-d8bd42b9bcd9.png">
この画面で表示されている天気の情報は、**APIで外部から取得**しているとします。
APIの実行には、どうしても多少時間がかかってしまうため、画面遷移直後には、API からのレスポンスが返ってくるまでの間、以下のように一瞬だけデータがない "**空白の画面**" が表示されてしまいます。

**問題の挙動**
<img width="550px" alt="一瞬データがない状態" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/c84cd837-63dd-8054-aaac-33f83daa6160.png">
API の実行に時間がかかってしまうことは仕方がありませんが、スムーズな画面遷移を装うために、できるだけこの"空白の画面"を表示したくありません。

そのため、一回目の画面表示時に取得したデータを破棄せず、**二回目以降の画面表示では、一回目で取得したデータを表示させてから、API で最新データを取得して差し替える**ことで、"空白の画面"を回避します。

**やりたい挙動**
<img width="550px" alt="二回目以降の表示" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/5c4977fb-295b-573e-d266-1508df5ea2b9.png">

この記事では、以下の 2つのステップで説明します。

* Step1: 取得したデータをキャッシュする方法
* Step2: 裏で API を実行してデータを取得する方法


**サンプルで利用しているAPIについて**

サンプルで利用している API は、Qiita の API v2 を利用しています。[^1]
説明の都合上、API を実行するたびに画面表示が変わる必要があるので、上記 API を実行し、レスポンスが返ってきた時間を表示しています。
（連打などは控えてください...）

# Step1: 取得したデータをキャッシュする方法
## コンポーネントの状態維持

通常、Vue Router による画面遷移や、なんらかの操作によって、コンポーネントを非表示にした場合、
コンポーネントのインスタンスは破棄され、コンポーネントインスタンス内のデータも消えてしまいます。[^2]
そのため、一度 API を利用して取得したデータも、コンポーネントが非表示になったタイミングで、破棄されてしまいます。
<img width="600px" alt="コンポーネントのインスタンスは破棄される" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/1279b337-bf07-f00d-eb62-eef86254ee59.png">

これを防ぐためには、`<keep-alive>` を利用します。
`<keep-alive>` で囲まれたコンポーネントは、**インスタンスが破棄されず、内部の状態も維持したままキャッシュ**されます。
<img width="600px" alt="コンポーネントのインスタンスは破棄されない" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/c90e57d7-0972-a4ed-8130-d22dbd3ec3b3.png">

## `<keep-alive>` のサンプルコード
`<keep-alive>` のサンプルコードを以下に用意しました。
入力フォームを含む子コンポーネントを、`<keep-alive>` なしと、`<keep-alive>` ありでそれぞれ呼び出しており、両者は上部の「表示切り替え」ボタンで表示/非表示が切り替わります。
`<keep-alive>` なしの方では、フォームに文字を入力したのち、表示/非表示を切り替えて、再度フォームを表示すると、非表示にしたタイミングでコンポーネントインスタンスが破棄されているため、入力した値が消えてしまっています。
しかし、`<keep-alive>` ありの方では、表示/非表示を切り替えても、入力した値が残っています。
これは、`<keep-alive>` ありの方では、**フォームを非表示にしたときにコンポーネントのインスタンスが、破棄されずに保持されている**ので、入力した文字のデータも残っているためです。

<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="html,result" data-user="y-kanoh" data-slug-hash="eYdybrp" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/eYdybrp">
  Vue.js keep-aliveについて</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

このように、`<keep-alive>` を利用することよって、一度 API で取得したデータを保持することができます。

## `<keep-alive>` の注意点
`<keep-alive>` を利用することで、破棄されるはずだったコンポーネントインスタンスがメモリ上に残ってしまうため、メモリリークが発生してしまう場合があります。
対策として、`<keep-alive>` のプロパティである、`include` や、`exclude` を指定することで、キャッシュするコンポーネントを正規表現や配列で指定し、不要なキャッシュを回避することができます。
また、`max` プロパティを指定することで、キャッシュするコンポーネントの最大数を指定する方法もあります。
詳しくは、`keep-alive` の[API リファレンス](https://jp.vuejs.org/v2/api/#keep-alive)を参照してください。
さらに、後述するライフサイクルフック `deactivated` を利用すれば、コンポーネントが非表示（非活性）になったタイミングで、不要なデータを削除し、メモリを節約することもできます。[^3]

# Step2: 裏で API を実行してデータを取得する方法
## `<keep-alive>` による弊害
`<keep-alive>` を利用することで、API で取得したデータを保持することはできました。
次に、コンポーネントを再表示したときに、裏で API を実行し、データを最新のものと差し替える処理を考えますが、ここで問題があります。
`<keep-alive>` を利用することで、コンポーネントは破棄さなくなったため、**コンポーネントが作成されるときに実行されるライフサイクルフックである `created` や、`monted` が利用できません。**[^4]


## `<keep-alive>` による弊害のサンプルコード
比較のために、まずは `<keep-alive>` を利用しない場合の例を用意しました。
以下の例は、`<keep-alive>` を利用__しないで__、子コンポーネントの `created` でAPIを実行し、レスポンスが返ってきた時刻を表示しています。
「表示の切り替え」ボタンを押して子コンポーネントを非表示にするたびに、コンポーネントはキャッシュされずに破棄されるため、再度「表示の切り替え」ボタンで子コンポーネントを表示すると、`created` が実行され、API が実行されてデータが再取得されます。

そのため、**"空白"は表示されますが、子コンポーネントの表示/非表示を切り替えるたびに、表示される時間が更新されています。**

<img width="730px" alt="keep-aliveなしcreatedで実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/5dfecc5e-16bf-aef2-088a-1cb4935455a5.png">

<p class="codepen" data-height="360" data-theme-id="dark" data-default-tab="js,result" data-user="y-kanoh" data-slug-hash="YzGYvwJ" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて（keep-alive なし）">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/YzGYvwJ">
  Vue.js keep-aliveについて（keep-alive なし）</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

一方、以下の例では、`<keep-alive>` で囲まれている子コンポーネントの `created` でAPIを実行し、レスポンスが返ってきた時刻を表示しています。
子コンポーネントは非表示になってもキャッシュされ、再度表示されるときに `created` は実行されません。

そのため、**子コンポーネントの表示/非表示を切り替えても、表示される時間が変わりません。**

<img width="730px" alt="keep-aliveありcreatedで実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/5212dceb-25fc-4042-cb88-e61d92f4449c.png">

<p class="codepen" data-height="360" data-theme-id="dark" data-default-tab="js,result" data-user="y-kanoh" data-slug-hash="MWjrXBE" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて（keep-alive あり）">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/MWjrXBE">
  Vue.js keep-aliveについて（keep-alive あり）</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

この場合、`<keep-alive>` で囲まれたコンポーネントが活性化するときにフックして処理を実行できるライフサイクルフックである、`activated` を利用します。

## `<keep-alive>` で追加されるライフサイクルフック
`<keep-alive>` を利用すると、以下のライフサイクルフックが利用できるようになります。

|ライフサイクルフック|実行タイミング|
|:--|:--|
|activated|`<keep-alive>` で囲まれたコンポーネントが活性化したとき|
|deactivated|`<keep-alive>` で囲まれたコンポーネントが非活性化したとき|

~~このライフサイクルフック、Vue.js の[ライフサイクルフック](https://jp.vuejs.org/v2/guide/instance.html#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF)のページに載ってなくて、最初気づけませんでした...~~

`activated` は、キャッシュしたコンポーネントが活性化したときに実行されるため、API でのデータ取得をここで実行することにより、**コンポーネントが表示されたタイミングで再度 API を実行することができます。**

## ライフサイクルフック `activated` を使ったサンプルコード
以下の例では、`<keep-alive>` で囲まれている子コンポーネントの `created` ではなく、`activated` にて API を実行し、レスポンスが返ってきた時刻を取得しています。

そのため、**`<keep-alive>` を利用していますが、子コンポーネントの表示/非表示を切り替えるたびに、表示される時間が更新されています。**

<img width="750px" alt="keep-aliveありcreatedで実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/7a962243-6a34-49bc-e382-6793f17679ac.png">

<p class="codepen" data-height="360" data-theme-id="dark" data-default-tab="js,result" data-user="y-kanoh" data-slug-hash="RwGxJYX" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて（keep-alive あり 更新あり）">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/RwGxJYX">
  Vue.js keep-aliveについて（keep-alive あり 更新あり）</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

これで、画面を切り替えたときには古いデータを表示し、その後 API で取得した最新データと差し替えることができます。

# キャッシュを利用した時と利用していない時の比較

以下は、先に記載したサンプルコードです。
**`<keep-alive>` を利用していないコード**では、子コンポーネント内に表示されるレスポンスが返ってきた時刻が、非表示から表示に切り替えたタイミングで、__一瞬、空白になることが確認できます。__
（すみません、通信環境によっては一瞬なので見えないかもしれません...）

一方、**`<keep-alive>` を利用しているコード**では、子コンポーネントを、非表示から表示に切り替えたタイミングで、__一瞬、前に表示した時刻が表示され、その後すぐに新しい時刻が表示されることが確認できます。__

### `<keep-alive>` を利用せず、再表示時には一瞬空白が表示されるコード
<img width="835px" alt="keep-aliveなしcreatedでAPI実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/04fda0d2-81c1-cab6-7c59-08ed71363eae.png">

<p class="codepen" data-height="360" data-theme-id="dark" data-default-tab="result" data-user="y-kanoh" data-slug-hash="YzGYvwJ" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて（keep-alive なし）">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/YzGYvwJ">
  Vue.js keep-aliveについて（keep-alive なし）</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

### `<keep-alive>` を利用し、再表示時の最初は古いデータを表示するコード
<img width="835px" alt="keep-aliveありactivatedでAPI実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176262/837390d9-e068-0982-9f5c-8ca57ae849a5.png">

<p class="codepen" data-height="360" data-theme-id="dark" data-default-tab="result" data-user="y-kanoh" data-slug-hash="RwGxJYX" style="height: 360px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue.js keep-aliveについて（keep-alive あり 更新あり）">
  <span>See the Pen <a href="https://codepen.io/y-kanoh/pen/RwGxJYX">
  Vue.js keep-aliveについて（keep-alive あり 更新あり）</a> by Y-KANOH (<a href="https://codepen.io/y-kanoh">@y-kanoh</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

# まとめ
ポイントは以下の2点です。
1. `<keep-alive>` を利用することで、コンポーネントインスタンスと、内部のデータをキャッシュできる
2. `<keep-alive>` でキャッシュしたコンポーネントインスタンスが活性になるタイミングで、`activated` ライフサイクルフックが実行される

1 を利用して API の実行結果をキャッシュし、
2 を利用して API を再度実行して古いデータと差し替える。

これにより、API のレスポンスが返ってくるまでに生じる ”空白の画面” を避けることができます。

# 参考
* https://jp.vuejs.org
  * [動的コンポーネントにおける keep-alive の利用](https://jp.vuejs.org/v2/guide/components-dynamic-async.html#%E5%8B%95%E7%9A%84%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%81%8A%E3%81%91%E3%82%8B-keep-alive-%E3%81%AE%E5%88%A9%E7%94%A8)
  * [keep-alive](https://jp.vuejs.org/v2/api/#keep-alive)
  * [インスタンスライフサイクルフック](https://jp.vuejs.org/v2/guide/instance.html#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF)
* [基礎から学ぶ Vue.js](https://www.amazon.co.jp/dp/B07D9BYHMZ/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)

[^1]: https://qiita.com/api/v2/docs#json-schema
[^2]: 要素に `display: none;` を付与するだけの、`v-show` ディレクティブによって非表示になった場合は除きます。
[^3]: https://jp.vuejs.org/v2/cookbook/avoiding-memory-leaks.html#%E4%BB%A3%E6%9B%BF%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
[^4]: https://jp.vuejs.org/v2/guide/instance.html#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF

<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>