Use a Vue service outside of a Vue component

193 Views Asked by At

I'm currently working on a vue 3 app using primevue 3.46. I'm wondering if it possible to use the ToastService to display a toast outside of a vue component.

Here is an example. I created an Axios instance so that i can intercept request and response errors. I would like to display a Toast if the status code is 401 (Unauthorized) or 403 (Forbidden). Tha fact that my axios interceptor is outside a vue component make it impossible to use the toast service because it provides a method for the Composition or the Option API.

Here is my main.ts file

// Core
import { createApp } from "vue";
import App from "@/App.vue";

// Styles
import "primevue/resources/themes/lara-dark-blue/theme.css";
import "primeflex/primeflex.css";
import "primeicons/primeicons.css";

// Plugins
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";

const app = createApp(App);

app.use(PrimeVue, { ripple: true });
app.use(ToastService);

app.mount("#app");

Here is my plugin/axios.ts file

import axios, { AxiosError } from "axios";

import { useToast } from "primevue/usetoast";

// Set default axios parameters
const instance = axios.create({
    baseURL: import.meta.env.VITE_API_BASE_URL,
});

instance.interceptors.response.use(
    (response) => response,
    (error: AxiosError) => {
        const toast = useToast(); // The error seems to be here
        if (error.response?.status === 401) {
            toast.add({ summary: "Unauthenticated", severity: "error" });
        } else if (error.response?.status === 403) {
            toast.add({ summary: "Forbidden", severity: "error" });
        } else {
            return Promise.reject(error);
        }
    }
);

export default instance;

The problem is that is always returns a vue error : [Vue warn]: inject() can only be used inside setup() or functional components.

Is there any way to use the ToastService outside a vue component like in an axios interceptor ?

1

There are 1 best solutions below

0
tao On BEST ANSWER

You can only use useToast() (or any other composable function) inside the setup function of a Vue component (or inside its <script setup>, which is the same thing), or inside another composable function (because it has the same context limitation).

But you can use a store (e.g: a reactive object, a pinia store, etc...) to trigger a change from the interceptor and then watch this change from a component responsible for rendering the alerts.

Generic example:

toastStore.ts

import { reactive, watch } from 'vue'

export const toastStore = reactive({
  toasts: []
})

// wrap this as composable, so you don't clutter the rendering component 
export const useToastStore = () => {
  const toast = useToast()
  watch(
    () => toastStore.toasts,
    (toasts) => {
      toasts.length && toast.add(toasts[toasts.length - 1])
    }
  )
}

axios.ts

import { toastStore } from './path/to/toastStore'
//...

instance.interceptors.response.use(
  (response) => response,
  (error: AxiosError) => {
    if (error.response?.status === 401) {
      toastStore.toasts.push({ summary: 'Unauthenticated', severity: 'error' })
    } else if (error.response?.status === 403) {
      toastStore.toasts.push({ summary: 'Forbidden', severity: 'error' })
    }
  }
)

App.vue 1:

import { useToastStore } from './path/to/toastStore'
export default {
  setup() {
    useToastStore()
    // rest of your App.vue's setup function...
  }
  // rest of your App.vue's component definition... 
}

If you use <script setup> in App.vue just place the call to useToastStore() inside the setup script.

The above uses a rather basic reactive object as store. If you already have a store in your app, you can use its state to store the toasts array. 2


1 - You don't have to place it in App.vue, you can use any other component, as long as its mounted when you push toasts to the store, so they get rendered.
2 - Note the code above will only show toasts pushed to the store while the rendering component (the one calling useToastStore) is mounted.
If you want a different behavior, you need to track which toasts have been shown to the user and which haven't, so you don't render the same toast more than once. That's why I suggested calling useToastStore in App.vue.