Search code examples
axiosvuejs3vue-composition-apiprimevuevue-options-api

Use a Vue service outside of a Vue component


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 ?


Solution

  • 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.