0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue3のSuspenseの使い方デモ

Posted at

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: 'タイトル1', content: '内容1'},
//   {title: 'タイトル2', content: '内容2'},
//   {title: 'タイトル3', content: '内容3'},
// ]

// 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: 'タイトル1', content: '内容1'},
  {title: 'タイトル2', content: '内容2'},
  {title: 'タイトル3', content: '内容3'},
]

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: '内容1'},
  {title: 'ParentsComp2', content: '内容2'},
  {title: 'ParentsComp3', content: '内容3'},
]

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は動かないです。そこがわかりにくいところかもしれません。

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

サンプルコードの説明

①について

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を定義する必要はなくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?