Vue3でスケルトン表示を実装する

`Vue3`でスケルトンコンポーネントを作成し、非同期処理中にそれを表示させる方法を紹介します。

以下の環境で実装しています。また、`Vue`は`Composition API`を採用しています。

```sh
vite/3.2.5 linux-x64 node-v16.14.2
vue: 3.2.45
```

# アニメーション付きのスケルトンコンポーネントを作成する
今回は大元となるスケルトンコンポーネントを自身で作成します。

以下のYouTube動画を参考に作成しています。

https://www.youtube.com/watch?v=2-jQ1v6X7vA&ab_channel=LearnVue

`props`として`height`,  `width`, `borderRadius`を設定します。
型を`string`としているのは、`100px`などの単位も含めて指定できるようにするためです。

`v-bind`を利用して、`props`から渡されたデータをCSSに適用しています。

```vue
<script setup lang="ts">
interface Props {
  height?: string;
  width?: string;
  borderRadius?: string;
}

// デフォルト値の設定
const props = withDefaults(defineProps<Props>(), {
  height: 'auto',
  width: '100%',
  borderRadius: '0%',
});
</script>

<template>
  <div class="skeleton">&nbsp;</div>
</template>

<style scoped>
.skeleton {
  height: v-bind('props.height');
  width: v-bind('props.width');
  border-radius: v-bind('props.borderRadius');
}
</style>
```

このコンポーネント（`AnimatedSkeleton`とします）を呼び出す際に、`height`,  `width`, `borderRadius`のいずれかを指定することで、呼び出し元に応じたスケルトンコンポーネントを作成できます。

```vue:呼び出し例
<template>
  <!-- 円のスケルトン -->
  <AnimatedSkelton height="100px" width="100px" borderRadius="50%" />
  <!-- 長方形のスケルトン -->
  <AnimatedSkelton height="10px" width="300px" />
</template>
```

CSSアニメーションも加えた`AnimatedSkeleton`の全体のソースは以下になります。

<details><Summary>AnimatedSkeleton.vue</Summary>

```vue:AnimatedSkeleton.vue
<script setup lang="ts">
// 呼び出し時にはheight, width, borderRadiusを指定（いずれも任意）
interface Props {
  height?: string;
  width?: string;
  borderRadius?: string;
}

// デフォルト値の設定
const props = withDefaults(defineProps<Props>(), {
  height: 'auto',
  width: '100%',
  borderRadius: '0%',
});
</script>

<template>
  <div class="skeleton">&nbsp;</div>
</template>

<style scoped>
// アニメーションの設定
@keyframes skeletonAnimation {
  0% {
    background-position: 50% 0;
  }
  100% {
    background-position: -100% 0;
  }
}
.skeleton {
  height: v-bind('props.height');
  width: v-bind('props.width');
  border-radius: v-bind('props.borderRadius');
  background-image: linear-gradient(to right, #d6d7d8 0%, #e2e3e4 10%, #d6d7d8 20%, #d6d7d8 100%);
  background-size: 200% 100%;
  animation: skeletonAnimation 1.2s linear infinite;
}
</style>
```
</details>

# 非同期処理実行中にスケルトンコンポーネントを表示する
今回はは、先述の`AnimatedSkeleton`と[Qiita API v2](https://qiita.com/api/v2/docs)を用いて、非同期処理が完了するまでの間に、スケルトンコンポーネントを表示する方法について解説します。

## ①`v-if`を使う方法

シンプルな方法です。
まず、今回取得するデータに`reactive`を使って初期値を設定します。

```vue:UserData.vue
<script setup lang="ts">
import { reactive } from 'vue';

const profile = reactive({
  profile_image_url: '',
  id: '',
  description: '',
});
</script>
```

以降はデータ取得処理を行い、取得が成功したら`profile`の各キーに取得データをセットします。

`<template>`内では「データがあればこの要素を表示し、なければスケルトンコンポーネントを表示する」というのを`v-if`を用いて以下のようにします。

```vue:UserData.vue
<template>
  <div>
    <!-- profile.profile_image_urlがtrueならimgを表示 -->
    <img
        v-if="profile.profile_image_url"
        :src="profile.profile_image_url"
        alt=""
      />
      <!-- profile.profile_image_urlがfalseならAnimatedSkeltonを表示 -->
      <AnimatedSkelton v-else height="100px" width="100px" borderRadius="50%" />
  </div>
</template>
```

`UserData.vue`のソースコードと画面表示は以下から確認ができます。

https://stackblitz.com/edit/github-uvko8p-3mm5yg?file=src/components/UserData.vue

## ②`<Suspense>`を使う方法

もう一つ、[`<Suspense>`](https://vuejs.org/guide/built-ins/suspense.html)を使った方法を紹介します。

:::note warn
執筆時点で[`<Suspense>`](https://vuejs.org/guide/built-ins/suspense.html)は実験的な機能のため、今後動作や仕様などが変更される可能性もあります。利用時にはその可能性を留意してください。
なお、以下の説明も執筆時点のものであり、最新の仕様とは異なる場合があります。
:::

`v-if`の時は、`UserData`コンポーネントの中の個々の要素に対してスケルトンコンポーネントを指定していました。今回は、`UserData`のレイアウトに丸ごと対応させた、`UserDataSkeleton`コンポーネントを作成します。

<details><Summary>UserDataSkeleton.vue</Summary>

```vue:UserDataSkeleton.vue
<script setup lang="ts">
import AnimatedSkelton from '@/components/AnimatedSkeleton.vue';
</script>

<template>
  <div class="container">
    <div class="author_img">
      <!-- UserDataの画像表示エリアに対応 -->
      <AnimatedSkelton height="100px" width="100px" borderRadius="50%" />
    </div>
    <div class="description">
      <div class="author_name">
        <!-- UserDataのユーザー名表示エリアに対応 -->
        <AnimatedSkelton height="2rem" width="8rem" />
      </div>
      <div>
        <!-- UserDataの紹介文表示エリアに対応 -->
        <AnimatedSkelton height="1rem" width="100%" />
        <AnimatedSkelton height="1rem" width="100%" />
        <AnimatedSkelton height="1rem" width="100%" />
      </div>
    </div>
  </div>
</template>
```

</details>

`<Suspense>`を利用する場合のコンポーネントとスケルトンコンポーネントの切り替えは、以下のように設定します。

```vue:UserDataView.vue
<script setup lang="ts">
import UserData from '@/components/UserData.vue';
import UserDataSkeleton from '@/components/UserDataSkeleton.vue';
</script>

<template>
  <main>
    <Suspense>
      <UserData />
      <!-- 非同期処理完了まではスケルトンを表示 -->
      <template #fallback>
        <UserDataSkeleton />
      </template>
    </Suspense>
  </main>
</template>
```


非同期コンポーネントである`UserData`コンポーネントが`resolved`になるまで、つまり非同期処理が完了するまで、`#fallback`で指定した`UserDataSkeleton`（スケルトンコンポーネント）を表示します。

`<Suspense>`を利用したソースコードの全容・画面表示は以下のリンクから確認ができます。

https://stackblitz.com/edit/github-uvko8p-e4baxv?file=src/views/UserDataView.vue


非同期処理中の制御を`<Suspense>`が担ってくれるため、`<Suspense>`はコンポーネントの中に複数の非同期コンポーネントが含まれる場合に特に有用です。

---

スケルトンコンポーネントの作成方法と、切り替え表示の方法について説明しました。

`<Suspense>`を使う方がコードがシンプルになりますが、まだ実験的機能であることを考えると実践で投入するのは難しいかもしれません。今後も`<Suspense>`の動向に着目していく必要があります:eyes: