フロントエンドエンジニアのこん(@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を使いましょう!ということですね。
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がありました。それがこちらです。
どうやら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の処理の順番として、以下のようになることが記載されています。
- Suspenseのdefaultコンテンツをメモリ上でレンダリング
- defaultコンテンツ内に非同期処理が含まれるとdefaultコンテンツがpendingになる
timeout
の値の時間経過後にSuspenseのfallbackコンテンツが表示される- defaultコンテンツ内の非同期処理が
resolve
される - 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にも対応できました!🥳
今回作ったものはこちらになります!🤲
おわりに
Vue3はまだまだ発展途上で、実験的な機能などは情報が少ないものもあり、調査に時間がかかるものもあります。はじめはわからないことが多くて辛いですが、絡まった糸を1本ずつ解くように調べたり試しに動かしてみることで、少しずつ核心に迫ることができると思います。そういった過程に楽しみを見いだせるとより良いコードやプロダクトづくりができるのかなと思う日々です。
ヤプリでは、このような取り組みを歓迎しております!興味を持った方はぜひ一度カジュアルにお話ししてみませんか?