📑

Nuxt2 -> Nuxt3移行 Loading の扱い

2023/07/14に公開

背景・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時点)を見てみました。

https://nuxt.com/docs/migration/component-options#loading

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は提供されていません。startfinishを手続き的に呼び出すコードを書いていた場合は、そのまま移行するにはNuxtプラグインを自作する必要があります。

Nuxtプラグイン の自作

Nuxtプラグイン

今回は子コンポーネントから操作できるようにするために、表示非表示の状態管理は、vueのアプリケーションレベルのprovide/injectで行います。Nuxtプラグインのprovideと混同して紛らわしいのですが、調べた限りはNuxtプラグインのprovideはreactiveな値自体をprovideできないようです。
(おそらくそのような使い方は想定されていない)。

plugins/00.loading.ts
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
      }
    }
  }
})

型定義

injectionKey.ts
import { InjectionKey, Ref } from "vue";

export type Loading = {
  isLoading: Readonly<Ref<boolean>>
  start: () => void
  finish: () => void
}

export const nuxtLoadingKey: InjectionKey<Loading> = Symbol("nuxt-loading")
types/nuxt-extend.d.ts
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 {}
}

参考
https://stackoverflow.com/questions/75046466/nuxt-3-extend-nuxtapp-type-with-custom-plugins

NuxtLoadingIndicatorの自作

独自にNuxtLoadingIndicatorを書き直す記事を書いた方がいるので、以下の記事も参考に拡張しました
https://zenn.dev/myuna/articles/cfcaeb0feb76da

本家のNuxtLoadingIndicatorthrottleの仕組みやらプログレスバーでやっていることもあり若干複雑に見えますが、重要なのは、Nuxtのグローバルhookで表示非表示のフラグを制御している部分だけです。

NuxtCustomLoadingIndicator.vue
<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