Nuxt2 -> Nuxt3移行 Loading の扱い
背景・Nuxt2のローディング
Nuxt2でローディングの機能を使っていた時にNuxt3に移行する話です。
Nuxt2のデフォルトの画面上部に表れるローダー
Nuxt2時代にはもともと、画面遷移時に画面上部にプログレスパーなどのIndicatorを表示することができました。nuxt.configで色を変えたりSpinkitにあるようなものをIndicatorとして指定したり、さらには、独自のローディングコンポーネントを指定することもできました。
プログラムから手続き的に呼び出すことも可能で、以下のようにデータフェッチ中に呼び出すこともNuxt2では可能でした。
export default {
async onClickButton () {
this.$nuxt.$loading.start()
try {
const response = await fetch('/api/v1/users')
....
} finally {
this.$nuxt.$loading.finish()
}
})
}
}
Nuxt2->3の移行
移行ドキュメント (2023/07/06時点)を見てみました。
This feature is not yet supported in Nuxt 3.
残念ながら、Nuxt3ではサポートされていないfeatureのようです。将来的にサポートする気があるのかもちょっと怪しいです。
さて、困りました。
NuxtLoadingIndicator
調べてみると、Nuxt3ではNuxtLoadingIndicatorというNuxtLinkなどと同様に使えるグローバルなコンポーネントが存在しました。デフォルトでは使われないため、使う場合は、layoutsかpageコンポーネントに追加する必要があります。
しかし、NuxtLoadingIndicatorはNuxt2の時のようにthis.$nuxt.$loading.start
,this.$nuxt.$loading.finish
といったAPIは提供されていません。start
やfinish
を手続き的に呼び出すコードを書いていた場合は、そのまま移行するにはNuxtプラグインを自作する必要があります。
Nuxtプラグイン の自作
Nuxtプラグイン
今回は子コンポーネントから操作できるようにするために、表示非表示の状態管理は、vueのアプリケーションレベルのprovide/injectで行います。Nuxtプラグインのprovideと混同して紛らわしいのですが、調べた限りはNuxtプラグインのprovideはreactiveな値自体をprovideできないようです。
(おそらくそのような使い方は想定されていない)。
import { nuxtLoadingKey } from '@/injectionKey'
import { defineNuxtPlugin } from 'nuxt/app'
import { ref, readonly } from 'vue'
export default defineNuxtPlugin((nuxtApp) => {
const isLoading = ref(false)
const start = () => {
isLoading.value = true
}
const finish = () => {
isLoading.value = false
}
// vueのアプリケーションレベルでのprovide
nuxtApp.vueApp.provide(nuxtLoadingKey,
{
isLoading: readonly(isLoading),
start,
finish
}
)
/**
* nuxtプラグインとしてのprovide
* nuxt2互換/nuxtApp.$loadingが使えるようにするため。
*/
return {
provide: {
loading: {
// ここでisLoadingを追加してもreactiveにならないので注意
start,
finish
}
}
}
})
型定義
import { InjectionKey, Ref } from "vue";
export type Loading = {
isLoading: Readonly<Ref<boolean>>
start: () => void
finish: () => void
}
export const nuxtLoadingKey: InjectionKey<Loading> = Symbol("nuxt-loading")
import { Loading } from "@/injectionKey";
interface PluginsInjections {
$loading: Loading
}
declare module '#app' {
interface NuxtApp extends PluginsInjections {}
}
declare module 'nuxt/dist/app/nuxt' {
interface NuxtApp extends PluginsInjections {}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties extends PluginsInjections {}
}
参考
NuxtLoadingIndicatorの自作
独自にNuxtLoadingIndicatorを書き直す記事を書いた方がいるので、以下の記事も参考に拡張しました
本家のNuxtLoadingIndicator
はthrottle
の仕組みやらプログレスバーでやっていることもあり若干複雑に見えますが、重要なのは、Nuxtのグローバルhookで表示非表示のフラグを制御している部分だけです。
<template>
<div v-if="isLoading">
<p>Custom Loading...</p>
</div>
</template>
<script lang="ts" setup>
import { useNuxtApp } from "nuxt/app"
import { nuxtLoadingKey } from "@/keys"
import { inject } from "vue";
const nuxtApp = useNuxtApp()
const { $loading } = inject(nuxtLoadingKey)
if ($loading == null) throw Error("nuxtLoadingKey is not provided")
const { isLoading, start, finish } = $loading
// Hook to app lifecycle
nuxtApp.hook('page:start', start)
nuxtApp.hook('page:finish', finish)
nuxtApp.hook('vue:error', finish)
onBeforeUnmount(finish)
</script>
page側でローダーを呼び出す
<template>
<button @click="onClickButton">ローダーを表示!</button>
</template>
<script lang="ts" setup>
// useNuxtAppから利用しているが、injectで直接使うこともできる
const { $loading } = useNuxtApp()
async function onClickButton() {
$loading.start()
try {
await fetch('/api/v1/users')
} finally {
$loading.finish()
}
}
</script>
余談 画面遷移とデータフェッチ中のローダー
今回のように画面遷移時とデータフェッチ中のローダーを共通にしてしまうと、画面遷移が終わる前に、子コンポーネントで$loading.finish()
が呼び出すなど、意図せぬ使われ方をして、制御が難しいことがあります。
なので、そもそも今回やりたいことの全否定になりますが、
- 画面遷移中とそれ以外のデータフェッチの時のローダーは共通にすべきか
- データフェッチ中にローダーを出すことが本当に必要か
- ローダーを出す場所が適切か
- リフレッシュしたい領域だけにローダーを出せば良いのではないか?(最近はsuspendの仕組みもある)
といったように、Nuxt3の移行を機に、本当にnuxtのLoadingでやるべきことだったのかを見直した方が良いかもしれません。
Discussion