🌀

VueのSuspenseが便利だったのでまとめてみた

2024/08/30に公開

はじめに

通信中にローディングアニメーションやスケルトンを表示したいことって珍しくないと思います。何も表示されない場合よりも何かしら表示された方が UX の向上につながるという話はよく言われる話です。
そこで今回、Vue でも簡単に通信中にフォールバックコンテンツを表示することができるSuspenseという機能を使ってみたところかなり便利だったので簡単にまとめてみたいと思います。

Suspense とは?

<Suspense> はコンポーネントツリー内の非同期な依存関係を処理する組み込みコンポーネントです。コンポーネントツリー内にある複数のネストされた非同期な依存関係が解決されるのを待っている間、ローディング状態をレンダリングできます。

と公式にあるように、<Suspense> は、Vue のコンポーネントツリー内で非同期な依存関係を扱うための組み込みコンポーネントです。具体的には、非同期コンポーネントや、非同期処理を行うsetupフックを持つコンポーネントがツリー内にある場合、それらの処理が解決されるまでローディング状態を表示する役割を果たします。
Vue3.0以降のバージョンであれば使用可能です。

例えば、通常はv-if="loading === true"などの状態変数を使ってローディングアニメーションを表示したりしますが、<Suspense>を使うと、そういった状態変数を使わずに、もっとシンプルにフォールバックコンテンツ(例えば Loading 中アイコン)を表示することができます。

なお、React にも同様のSuspenseという機能がありますが、Vue のものとは異なり、React ではすでに安定版として利用されています。

https://ja.vuejs.org/guide/built-ins/suspense#suspense

そもそも非同期コンポーネントって?

非同期コンポーネントは、Vue.js で必要なときにだけコンポーネントをロードする仕組みです。大きなアプリケーションでは、すべてのコンポーネントを一度にロードすると、初期表示に時間がかかることがあります。そこで、非同期コンポーネントを使うと、ユーザーが特定のページにアクセスしたタイミングでそのページ用のコンポーネントをロードし、パフォーマンスを向上させることができます。

Suspense と非同期コンポーネントの関係

Vue の<Suspense>は、こうした非同期コンポーネントと相性が抜群です。
解説が重複しますが、例えば、ページの一部が非同期で読み込まれる場合、<Suspense>でその部分をラップしておくと、すべての非同期処理が完了するまでローディングスピナーなどのフォールバックコンテンツを表示してくれます。

具体的には、次のようなコンポーネント階層を考えてみましょう(公式より引用)。

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus>(async setup() を使用したコンポーネント)
   └─ <Content>
      ├─ <ActivityFeed>(非同期コンポーネント)
      └─ <Stats>(非同期コンポーネント)

このように、いくつかのコンポーネントが非同期に依存していると、通常はそれぞれが独自にローディングやエラーの処理を行う必要があります。しかし、<Suspense>を使うことで、これらを統一的に管理し、ユーザーには 1 つのローディング画面だけを見せることができるのです。これにより、ユーザー体験が一貫性を保ち、複数のローディングスピナーがバラバラに表示されるといった混乱を防げます。

https://ja.vuejs.org/guide/built-ins/suspense#async-dependencies

<Suspense> が待ち受けることができる非同期な依存関係は 2 種類

  • 非同期 setup()フックを持つコンポーネント。これには、トップレベルの await がある <script setup> を使用したコンポーネントも含まれます。
  • 非同期コンポーネント

の 2 種類の依存関係をSuspenseは待ち受けることができます。
非同期コンポーネントは上記で解説しましたが、

非同期 setup()フックを持つコンポーネント。これには、トップレベルの await がある <script setup> を使用したコンポーネントも含まれます。

の部分がいまいちよくわからないと思うので簡単に説明します。

async setup()

Composition API のコンポーネントの setup() フックはasyncを使用することで非同期にすることができます。

export default { async setup() { const res = await fetch(...) const posts =
await res.json() return { posts } } }

https://ja.vuejs.org/guide/built-ins/suspense#async-setup

・トップレベルのawaitがある<script setup>

トップレベルの await があると、そのコンポーネントは自動的に非同期な依存関係になります

<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>

<template>
  {{ posts }}
</template>

https://ja.vuejs.org/guide/built-ins/suspense#async-components

まとめると、Composition API を使用してそのコンポーネントを非同期にする場合、

  • async setup()を使用する
  • <script setup>を使用する場合はトップレベルにawaitを記述する

ことで非同期コンポーネントにすることができるということです。

Suspense を実際に使ってみよう

環境など

  • Vue.js
    • 最新版である 3.4.29
    • script setupを TypeScript で書いています。
  • 使用 API

https://jsonplaceholder.typicode.com/

また、実装に使用したコードは以下のリポジトリから確認できます。
https://github.com/hiroto0701/vue-suspense-sample

ディレクトリ構成

最低限のコードで以下のような構成で解説を進めます。

src
├─ components
│  └─ AsyncUsers.vue
├─ composables
│  └─ useFetchUsers.ts
├─ types
│  └─ user.ts
└─ App.vue
ツリー作成で使用したツール

余談ですが上記のツリーはdir-makerを使用しました。
かなり便利でヘビーユーザーになってます。
開発してくださってありがとうございます 😢

https://zenn.dev/praha/articles/b2e225ae091ae3

https://dir-maker.netlify.app/

以下のように、<Suspense>コンポーネントでラップして、内部の<template #default>内に、非同期コンポーネントを、<template #fallback>内に、フォールバックコンテンツを記載します。
それだけで、非同期コンポーネントの処理が解決するまでフォールバックコンテンツを描画します。

この場合だと、<AsyncUsers>の非同期処理が終わるまで、ユーザーデータを読み込み中...という文字列を表示してくれます。

App.vue
<script setup lang="ts">
import AsyncUsers from './components/AsyncUsers.vue'
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncUsers />
    </template>
    <template #fallback>
      <div>ユーザーデータを読み込み中...</div>
    </template>
  </Suspense>
</template>

以降は使用法を重点的に解説していきます。

<Suspense>を使用する親コンポーネント(App.vue)

App.vue
<script setup lang="ts">
import AsyncUsers from './components/AsyncUsers.vue'
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncUsers />
    </template>
    <template #fallback>
      <div>ユーザーデータを読み込み中...</div>
    </template>
  </Suspense>
</template>

非同期な処理が完了したらAsyncUsersを表示、通信中は「ユーザーデータを読み込み中...」という文章を表示します。

非同期コンポーネント(AsyncUsers.vue)

user.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export type UsersResponse = User[];
AsyncUsers.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useFetchUsers } from '../composables/useFetchUsers';
import type { User } from '../types/user';

const { fetchUsersList } = useFetchUsers();
const users = ref<User[]>([]);
const error = ref<string | null>(null);

async function load() {
  try {
    await new Promise(resolve => setTimeout(resolve, 5000));
    users.value = await fetchUsersList();
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'エラーが発生しました。';
  }
}

await load();
</script>

<template>
  <div>
    <h2>ユーザー一覧</h2>
    <div v-if="error">エラー: {{ error }}</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} ({{ user.email }})
      </li>
    </ul>
  </div>
</template>

トップレベルawaitを使用してfetchUsersList()を呼び出すload()を実行しています。
load()では通信中というのを分かりやすくするために5秒間のタイムラグを埋め込んでいます。

API と通信を行う(useFetchUsers.ts)

今回はJSON Placeholderを使用してユーザーのダミーデータを取得するような処理をfetch APIを使用してコンポーザブルな関数として実装しています。

Vue アプリケーションの文脈で「コンポーザブル(composable)」とは、Vue の Composition API を活用して状態を持つロジックをカプセル化して再利用するための関数です。

直接コンポーネント内から呼び出すのもいいですが、再利用性を考えてコンポーザブルな関数として定義しています。
コンポーザブルについては深く解説しませんが、引数などをいじれば取得するデータをコンポーネントごとに変えられるけど、ロジックはそのまま使う、というようなことが可能になります。

useFetchUsers.ts
import type { UsersResponse } from '../types/user';

export const useFetchUsers = () => {
  async function fetchUsersList(): Promise<UsersResponse> {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    if (!response.ok) {
      throw new Error('Failed to fetch users');
    }
    return response.json();
  }

  return { fetchUsersList };
};

実装の結果

実装の結果が以下のgifです。(※コンテンツの表示までに少し時間がかかります。)


上記のように通信中はフォールバックコンテンツを表示し、通信完了後にユーザー情報が表示されています。

最後に

Suspenseを使用することでかなり簡単に非同期処理時のフォールバックコンテンツ表示を実装することができました。
ただ、現段階では試験的な機能なので、是非とも今後のアップデートで安定版として採用されて欲しいと切に願っております。

皆さんも Vue を使用する際はぜひ使ってみて下さい。

最後まで読んでいただきありがとうございました!

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion