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