Zenn
🍞

【Nuxt3】どこからでも呼び出せるトーストを作成してみた

2025/03/26に公開

右下にトーストを表示させる話で、色々と使い道がある関係でpagescomponentsのvueファイルに記述するとクローンコードだらけになってしまいそうなので、app.vueに設置した話。

こんなトースト

環境

OS: MacOS
Node: v22.14.0
Nuxt: 3.15.4
Vue: latest
@nuxtjs/tailwindcss: 6.13.1
自作の社内コンポーネントライブラリ

Editor: Cursor

トーストのpluginを作る

composablesかpluginsかという分かりにくさがNuxtにはありますが、
トーストの表示する文章などを指定するステートレスな関数にしたいためpluginsを採用しました。🍍piniaは↑の解説を見る限りにDBっぽい使われ方をするものなので、データを裏で保持しておく必要のない今回は使っていません。

toastshowtoastの二つをprovideしています。

plugins/toast.ts
import { reactive } from 'vue'

export type ToastState = {
  label: string
  message: string
  variant: 'success' | 'error' | 'warning' | 'info'
  show: boolean
  cancelable: boolean
}

//showの値で表示非表示を決めています
export const toastState = reactive<ToastState>({
  label: '',
  message: '',
  variant: 'info',
  show: false,
  cancelable: false
})

//自作のコンポーネントに渡すprops
interface ToastOptions {
  label: string
  message: string
  variant?: ToastState['variant']
  cancelable?: boolean
}

export const showToast = ({label, message, variant = 'info', cancelable = false}: ToastOptions) => {
  toastState.label = label
  toastState.message = message
  toastState.variant = variant as ToastState['variant']
  toastState.show = true
  toastState.cancelable = cancelable

  if(!cancelable) {
    setTimeout(() => {
      toastState.show = false
    }, 3000)
  }
}
// cancelable === true時に使うcloseToastはまだ作ってない
export default defineNuxtPlugin(() => {
  return {
    provide: {
      toast: toastState,
      showToast
    }
  }
})

トーストを表示させる

先ほど作ったpluginを使って、実際にトーストを表示させます。
まずトーストを表示させる場所については、全ページで使うためapp.vueに設置します。

  • fixedを使うことで画面の所定の位置(右下)に表示されるように
  • トーストはクライアントサイドでレンダリングされる機能なのでClientOnlyを使うことでHydrationエラーを防ぐ
app.vue
<script setup lang="ts">
import { Toast } from 'my-template';

const { $toast } = useNuxtApp()

</script>

<template>
  <NuxtLayout>
    <NuxtPage />
    <ClientOnly>
      <div v-if="$toast.show" class="w-[400px] fixed bottom-4 right-4">
        <Toast :label="$toast.label" :message="$toast.message" :variant="$toast.variant" :cancelable="$toast.cancelable" />
      </div>
    </ClientOnly>
  </NuxtLayout>
</template>

これで$toast.show === trueとなるとトーストが表示されるようになったので、showtoast関数を使ってトーストを表示させます。
記事の冒頭のスクショの、ログインが必要なページに非ログイン状態でアクセスしようとした際の処理はこんな感じになります。

login.vue
const { $showToast } = useNuxtApp();
const { t } = useI18n();

const route = useRoute();
if (route.query.reason) {
  switch (route.query.reason) {
    case "unauthorized":
      $showToast({
        label: t("toast.auth.unauthorized.label"), //vue-i18n
        message: t("toast.auth.unauthorized.message"),
        variant: "error",
      });
      break;
    case "session_expired":
      $showToast({
        label: t("toast.auth.session_expired.label"),
        message: t("toast.auth.session_expired.message"),
        variant: "error",
      });
      break;
    default:
      break;
  }
}
middleware/authenticated.global.ts
  const { loggedIn } = useUserSession() //Nuxt-Auth-Utilsの関数

  if (!loggedIn.value && to.path !== '/login') {
    return navigateTo({ 
      path: '/login', 
      query: { reason: 'unauthorized' },
      replace: true
    })
  }

まとめ:props/emit地獄からの脱出

過去にvueでコーディングしていた時はcomposablesやpluginsの概念を知らなかったので、親コンポーネント/ページで使っているコンポーネントにデータを渡すために、$emit地獄になっていたか、vuexあるいはpiniaといったstoreを使って、常に保持しているデータに気を使って開発しないといけない状況になっていました。
pluginsとTS[1]の組み合わせで、これだけ楽に各ページ、コンポーネントで何も気にせずに呼び出せるようになって感動しています。

脚注
  1. 型指定=必須パラメータを指定することでメッセージがnullになったり前のトーストのものになってしまう問題を防ぐ ↩︎

Discussion

ログインするとコメントできます