Vue3のSuspenseの使い方デモ

## Vue3のSuspenseの使い方

Vue3のSuspenseについて、どういう風に使うことができるのか理解するために触ってみました。

公式ドキュメント
https://vuejs.org/guide/built-ins/suspense.html#events

## 結論
非同期コンポーネントをローディング中に、ローディング中には非同期コンポーネントとは別のコンポーネントを表示し、ローディング後には非同期コンポーネントを表示するように切り替えすることができる機能。

例えば、
データテーブルの表示をするときにAPIを叩いてデータが取得できればデータ表示。
エラーになればエラーの表示。
という分岐をしてコンポーネントを作ると思います。

その際の読み込み中、データ表示、エラー表示の切り替えのところをSuspenseを使って書くことができます。

## コードサンプル
```src/App.vue
<template>
  <div class="wrapper">
    <!-- ①Suspenseを使った場合 -->
    <div v-if="error">{{ error }}</div>
    <Suspense v-else>
      <template #default>
        <DataList></DataList>
      </template>
      <template #fallback>
        Loading...
      </template>
    </Suspense>

    <!-- ②Suspenseを使わない場合 -->
    <div v-if="isLoading && !error">非同期処理が終わった後に表示されるデータ</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>Loading...</div>

    <!-- ③親で取得したデータを渡したいが、非同期コンポーネントじゃなくなるのでこれは機能しない -->
    <!-- <Suspense>
      <template #default>
        <DataList2 :lists="lists"></DataList2>
      </template>
      <template #fallback>
        Loading...
      </template>
    </Suspense> -->

    <!-- ④親で取得したデータを渡したいときはもう一つコンポーネントを挟むことになる -->
    <div v-if="error">{{ error }}</div>
    <Suspense v-else>
      <template #default>
        <ParentsComp></ParentsComp>
      </template>
      <template #fallback>
        Loading...
      </template>
    </Suspense>
  </div>
</template>

<script setup lang="ts">
import DataList from './components/DataList.vue'
// import DataList2 from './components/DataList2.vue'
import ParentsComp from './components/ParentsComp.vue'
import { ref, onErrorCaptured } from 'vue'

// const mockData = [
//   {title: 'タイトル１', content: '内容１'},
//   {title: 'タイトル２', content: '内容２'},
//   {title: 'タイトル３', content: '内容３'},
// ]

// const fetchMockData = () => {
//   return new Promise<{ title: string; content: string }[]>((resolve) => {
//     setTimeout(() => {
//       resolve(mockData)
//     }, 3000)
//   })
// };
// const lists = await fetchMockData();

const isLoading = ref(false);
setTimeout(() => {
  isLoading.value = true;
}, 3000)

const error :any = ref(null);
onErrorCaptured((e) => {
  error.value = e
  return true;
});
</script>

<style scoped>
.wrapper {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
}
.wrapper > div {
  flex: 1;
}
</style>
```

```src/components/DataList.vue
<template>
  <div>
    <div v-for="(list, key) in lists" :key="key">
      <h1>{{ list.title }}</h1>
      <div>{{ list.content }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
const mockData = [
  {title: 'タイトル１', content: '内容１'},
  {title: 'タイトル２', content: '内容２'},
  {title: 'タイトル３', content: '内容３'},
]

const fetchMockData = () => {
  return new Promise<{ title: string; content: string }[]>((resolve, reject) => {
    setTimeout(() => {
      resolve(mockData)
      // エラーハンドリングを確かめたい時はコメントアウトを外してください
      // reject(new Error("データを取得できませんでした"));
    }, 3000)
  })
};
const lists = await fetchMockData();
</script>
```

```src/components/DataList2.vue
<template>
  <div>
    <div v-for="(list, key) in lists" :key="key">
      <h1>{{ list.title }}</h1>
      <div>{{ list.content }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  lists: { title: string; content: string }[]
}>();
</script>
```


```src/components/ParentsComp.vue
<template>
<DataList2 :lists="lists"></DataList2>
</template>

<script setup lang="ts">
import DataList2 from './DataList2.vue'
import { ref } from 'vue'

const mockData = [
  {title: 'ParentsComp', content: '内容１'},
  {title: 'ParentsComp２', content: '内容２'},
  {title: 'ParentsComp３', content: '内容３'},
]

const fetchMockData = () => {
  return new Promise<{ title: string; content: string }[]>((resolve, reject) => {
    setTimeout(() => {
      resolve(mockData)
      // エラーハンドリングを確かめたい時はコメントアウトを外してください
      // reject(new Error("データを取得できませんでした"));
    }, 3000)
  })
};
const lists = await fetchMockData();
</script>

<style scoped>
</style>
```

今回は仮でsetTimeoutを使ってローディングを表現しています。

このSuspenseを使う対象のコンポーネントは非同期コンポーネントでないとSuspenseは動かないです。そこがわかりにくいところかもしれません。

公式ドキュメント：非同期コンポーネント

https://ja.vuejs.org/guide/components/async.html#basic-usage

## サンプルコードの説明

### **①について**

DataList.vueでtop level awaitを使って非同期処理が行われており、ライフサイクルのonCreatedのところでsetTimeoutの3秒待ちの処理が入ります。3秒経った後にmockDataを返してコンポーネントが表示されます。

その3秒待ちの間、Suspenseの#fallback側のテンプレートが表示されます。

### **②について**

このローディング中のテンプレート切り替えの処理はSuspenseを使わなかった場合は、②のようにv-ifで切り替えていたと思います。その処理をSuspenseに変えることができます。

### **③について**

また、③のように親でAPIを叩いてデータ取得し、子にPropsでデータを渡して表示するという場合の時のやり方をしてみました。

この場合だと子コンポーネントが非同期コンポーネントではなくなってしまうのでSuspenseは動かないです。

### ④について

上記のPropsでデータを渡すパターンを行いたいときは④のようにもう一つコンポーネントを挟むことになってしまうと思います。

### エラーハンドリングについて

エラーハンドリングは公式にある通り、ライフサイクルフックのonErrorCapturedを使います。

サンプルコードの通り、エラー時とエラーじゃない場合の切り替えは結局v-ifを使うことになります。

エラーハンドリングを試したい場合はfetchMockDataのreject部分をコメントアウトしてチェックできます。

# 最後に

以上、Suspenseとは結局何なのか理解するためにデモをしてみました。

Suspenseを使えばisLoadingといった分岐のためのstateを定義する必要はなくなります。