Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

VueのSuspenseとRouterViewの組み合わせで正しくfallbackさせる

フロントエンドエンジニアのこん(@k0n_karin)です!

今回は昨今のVueに欠かせない2つの便利なコンポーネントを組み合わせて使う際のお作法を書いていきます。

VueのSuspense

Vue3からはcomposition APIでsetup()<script setup>を使う事が増えたと思います。
setup内で非同期な処理を実行することも日常茶飯事だと思いますが、そのまま実装してしまうと、以下のような警告が出て正しくコンポーネントが表示されません。

[Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.

せっかくなのでChatGPTにいい感じに訳してもらいました。自然でわかりやすい日本語に訳してくれるので驚きます。

[Vue warn]: コンポーネント <Anonymous>: setup関数がPromiseを返しましたが、親のコンポーネントツリーに<Suspense>が見つかりませんでした。非同期なsetup()を持つコンポーネントは、<Suspense>内にネストされている必要があります。

つまりSuspenseを使いましょう!ということですね。

ja.vuejs.org

Suspenseは実験的な機能ですが、setup()内の非同期処理を解決してくれるだけでなく、非同期処理を待っている間にローディング状態もレンダリングしてくれます。🙌

<template>
  <Suspense>
    <!-- setup 内のPromiseがresolveされたら表示 -->
    <PromiseView></PromiseView>

    <!-- Promiseが解決されるまでfallbackコンテンツを表示 -->
    <template #fallback>Loading...</template>
  </Suspense>
</template>

SuspenseとRouterViewを同時に使う

さて、今回のメインのお話に入っていきます。

RouterViewで表示される各routeのコンポーネントにおいて、コンポーネント内に非同期なsetup()を含むケースではSuspenseとRouterViewを同時に使う必要があります。これはVueのSuspenseのドキュメントの最後の方に記載されています。

<!-- 今回の場合 -->
<RouterView v-slot="{ Component }">
  <Suspense>
    <!-- main content -->
    <component :is="Component"></component>

    <!-- loading state -->
    <template #fallback>
      Loading...
    </template>
  </Suspense>
</RouterView>

しかし、これだけだと正しくfallbackされず、Loading…は表示されません。😢

RouterView内のSuspenseのfallbackコンテンツを表示させる

こういう時に頼れる公式リポジトリのissueを見ていくわけですが、ちょうどビンゴなissueがありました。それがこちらです。

github.com

どうやらSuspenseにtimeoutを設定する必要があるようです。

<RouterView v-slot="{ Component }">
  <Suspense timeout="0">
    <!-- main content -->
    <component :is="Component"></component>

    <!-- loading state -->
    <template #fallback>Loading...</template>
  </Suspense>
</RouterView>

それぞれ特徴的な箇所を見ていきましょう。

まず、RouterViewで動的に変わる子コンポーネントをv-slotディレクティブのComponentプロパティから取得します。型定義はこちらです。v-slotについては、こちらのドキュメントが参考になります。

そして、Suspense内のdefaultコンテンツとしてcomponentタグを使い、isプロパティで動的にroute毎のコンポーネントを表示させます。

次に、Suspenseにtimeoutプロパティを指定します。よく見ると、Suspenseのページにtimeoutの記載がありました。(初めて知りました。
ドキュメントを読むと、timeout設定時のSuspenseの処理の順番として、以下のようになることが記載されています。

  1. Suspenseのdefaultコンテンツをメモリ上でレンダリング
  2. defaultコンテンツ内に非同期処理が含まれるとdefaultコンテンツがpendingになる
  3. timeoutの値の時間経過後にSuspenseのfallbackコンテンツが表示される
  4. defaultコンテンツ内の非同期処理がresolveされる
  5. defaultコンテンツが表示される

つまり、timeout="0"にすることで、即座にfallbackコンテンツを表示し、resolveされてからdefaultコンテンツを表示するように見せることができます。🥳
逆に、一定時間後にfallbackコンテンツを表示させたい場合には、timeoutを長めに設定するのも一つです。

Suspenseのwarningに対応する

ここまでで、RouterView内のSuspenseのfallbackコンテンツを表示できましたが、ブラウザのコンソールを見てみると、1件のWarningが出ています。

[Vue warn]: <Suspense> slots expect a single root node.

これはChatGPTさんに聞くまでもなく、Suspenseには単一のルートノードにしてねという警告です。
Vue3からは、コンポーネントに複数のルートノードを持てるようになりました。componentタグは単一のタグになりますが、componentタグによって動的に表示されるコンポーネントが複数のルートノードを持つ可能性があります。そのため、このような警告が出ていると考えられます。

ドキュメントを漁ってみると、「Transitionコンポーネントでは単一ルートノードにしてね〜!」という案内がありました。きっとSuspenseも同様なのだと思います。

<Transition> はスロットのコンテンツとして、単一の要素またはコンポーネントのみをサポートします。コンテンツがコンポーネントの場合、そのコンポーネントも単一のルート要素のみでなければなりません。
https://ja.vuejs.org/guide/built-ins/transition.html#the-transition-component

Nuxt3でも同じように、単一のルートノードにしてね!という警告が出るケースがあるようです。

Pages must have a single root element to allow route transitions between pages. (HTML comments are considered elements as well.)
https://nuxt.com/docs/guide/directory-structure/pages#usage

この警告に対応するには、componentタグをdivタグなどで囲う必要があります。
ただし、単純にdivタグで囲うだけだと再びfallbackコンテンツが表示されなくなります。Suspenseがコンポーネントの変更を追跡できるように、一意のkeyを付与してあげる必要があるようです。

<Suspense timeout="0">
  <div :key="route.path">
    <component :is="Component" />
  </div>
  <template #fallback>
    <div>Loading...</div>
  </template>
</Suspense>

これでWarningにも対応できました!🥳

今回作ったものはこちらになります!🤲

github.com

おわりに

Vue3はまだまだ発展途上で、実験的な機能などは情報が少ないものもあり、調査に時間がかかるものもあります。はじめはわからないことが多くて辛いですが、絡まった糸を1本ずつ解くように調べたり試しに動かしてみることで、少しずつ核心に迫ることができると思います。そういった過程に楽しみを見いだせるとより良いコードやプロダクトづくりができるのかなと思う日々です。
ヤプリでは、このような取り組みを歓迎しております!興味を持った方はぜひ一度カジュアルにお話ししてみませんか?

open.talentio.com