英文:
use pinia outside of component raises no active pinia error
问题
我正在使用 Vue.js 3 项目,使用 ViteSse 模板构建。我想在 setup 组件外部使用我的一个 Pinia 存储,名为 notificationStore
,但是当我运行 dev 命令时,出现了错误 getActivePinia was called with no active Pinia. Did you forget to install pinia
。这个错误的行为非常奇怪,因为如果我在 dev 服务器加载后添加 useNotificationStore
,它就不会出现错误,而且能正常工作。但是如果 useNotificationStore
存在于我的 ts 文件中并且运行 dev 命令,就会出现错误。
我已经使用日志追踪了问题,并发现它在 createApp
被执行之前发生,而 createApp
初始化了 pinia,但是在那个时候没有激活的 pinia。我期望 main.ts
中的 createApp
在所有其他代码之前执行。在此提前感谢。
英文:
I'm working on a Vue.js 3 project build using ViteSse template, I wanna use one of my Pinia stores named notificationStore outside of the setup component but I get getActivePinia was called with no active Pinia. Did you forget to install pinia
error when I run dev command It's behavior is so strange becasue it's not occured if I add useNotificationStore after dev server loaded and It woks well but when useNotificationStore exisit in my ts file and run dev command it raise error.
here is my setup:
main.ts
import { ViteSSG } from 'vite-ssg'
import { setupLayouts } from 'virtual:generated-layouts'
import { useAuth, useOidcStore } from 'vue3-oidc'
import type { UserModule } from './types'
import App from './app.vue'
import generatedRoutes from '~pages'
import './assets/styles/style.scss'
import '@/config/oidc'
const routes = setupLayouts(generatedRoutes)
const { autoAuthenticate } = useAuth()
const { state } = useOidcStore()
const isMocking = import.meta.env.VITE_API_MOCKING_ENABLED
if (isMocking === 'true') {
const browser = await import('~/mocks/browser') as any
browser.worker.start({ onUnhandledRequest: 'bypass' })
}
export const createApp = ViteSSG(
App,
{ routes, base: import.meta.env.BASE_URL },
async (ctx) => {
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
.forEach(i => {
i.install?.(ctx)
})
const DEFAULT_TITLE = 'Dashboard'
ctx.router.beforeEach((to) => {
if (!state.value.user) {
autoAuthenticate()
}
let title = DEFAULT_TITLE
if (to.meta.title)
title = `${to.meta.title} - ${title}`
document.title = title
})
},
)
notification.store.ts
import { acceptHMRUpdate, defineStore } from 'pinia'
interface NotificationState {
messages: Array<Notification>
}
export interface Notification {
type?: 'error' | 'info' | 'success' | 'warning'
timeout?: number
permanent?: boolean
message: string
hideAfterRouting?: boolean
validationErrors?: Array<string>
shown?: boolean
}
export const useNotificationsStore = defineStore('notification', {
state: (): NotificationState => ({
messages: [],
}),
actions: {
addNotification(message: Notification) {
if (isDuplicatedMessage(this.messages, message))
return
message.timeout = message.timeout || 3000
message.shown = true
this.messages.push(message)
},
remove(index: number) {
this.messages.splice(index, 1)
},
clearAfterRoute() {
const visibleNotification = this.messages.filter(
item => item.hideAfterRouting === false,
)
this.messages = visibleNotification
},
},
})
function isDuplicatedMessage(messages: Notification[], message: Notification): boolean {
if (!messages.length)
return false
return (messages[messages.length - 1].message === message.message)
}
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useNotificationsStore, import.meta.hot))
http-client.ts
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'
import { useNotificationsStore } from '~/store/notification.store'
const notification = useNotificationsStore()
const httpClient: AxiosInstance
= axios.create({
baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
headers: {
'Content-Type': 'application/json',
},
})
httpClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = tokenService.getLocalAccessToken()
if (token && config.headers)
config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => {
return Promise.reject(error)
},
)
httpClient.interceptors.response.use(
(response) => {
if (response.config.method !== 'get') {
const successMsg = 'operation successfully completed'
notification.addNotification({ type: 'success', message: successMsg })
}
return response
},
(error) => {
if (error.message === 'Network Error') {
notification.addNotification({ type: 'error', message: 'Network Error' })
return Promise.reject(error)
}
let errorMessage = ''
const validationErrors = getValidationErrors(error)
errorMessage = getErrorMessage(error)
notification.addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
}
)
Vite.config.ts
// Plugins
// Utilities
import path from 'path'
import { URL, fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import vuetify from 'vite-plugin-vuetify'
import Vue from '@vitejs/plugin-vue'
import Pages from 'vite-plugin-pages'
import Layouts from 'vite-plugin-vue-layouts'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { VitePWA } from 'vite-plugin-pwa'
import VueI18n from '@intlify/vite-plugin-vue-i18n'
import Inspect from 'vite-plugin-inspect'
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 9080,
host: true,
hmr: {
host: 'localhost',
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'~': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
plugins: [
Vue({
include: [/\.vue$/],
reactivityTransform: true,
}),
vuetify({
autoImport: true,
}),
// https://github.com/hannoeru/vite-plugin-pages
Pages({
extensions: ['vue'],
dirs: [
{ dir: 'src/router/views', baseRoute: '' },
],
extendRoute: (route) => {
if (route.path === '/')
return { ...route, redirect: '/Dashboard' }
return route
},
}),
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
Layouts({
layoutsDirs: ['src/router/layouts'],
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
'vue',
'vue-router',
'vue-i18n',
'vue/macros',
'@vueuse/head',
'@vueuse/core',
],
dts: 'src/auto-imports.d.ts',
dirs: [
'src/composable',
'src/store',
],
vueTemplate: true,
}),
// https://github.com/antfu/unplugin-vue-components
Components({
extensions: ['vue'],
include: [/\.vue$/, /\.vue\?vue/],
dts: 'src/components.d.ts',
deep: true,
}),
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
include: [path.resolve(__dirname, 'locales/**')],
}),
// https://github.com/antfu/vite-plugin-inspect
// Visit http://localhost:3333/__inspect/ to see the inspector
Inspect(),
],
// https://github.com/vitest-dev/vitest
// test: {
// include: ['test/**/*.test.ts'],
// environment: 'jsdom',
// deps: {
// inline: ['@vue', '@vueuse', 'vue-demi'],
// },
// },
// https://github.com/antfu/vite-ssg
ssgOptions: {
script: 'sync',
formatting: 'minify',
// onFinished() { generateSitemap() },
},
ssr: {
// TODO: workaround until they support native ESM
noExternal: ['workbox-window', /vue-i18n/],
},
define: { 'process.env': {} },
})
I have traced issue with some logging and finded that it occured before createApp get executed that initate pinia and there was not active pinia on that time. I expected createApp in main.ts have been executed before all
thanks in advance
答案1
得分: 1
我通过仔细阅读文档成功解决了这个问题:
> 确保这总是被应用的最简单方法是将 useStore() 的调用延迟到在 pinia 安装后总是会运行的函数内部。
我通过将文件顶部的 useNotificationStore() 移除,并在需要 notificationStore 实例的地方使用 useNotificationStore().addNotification(...) 来更新 http-client。
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'
const httpClient: AxiosInstance
= axios.create({
baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
headers: {
'Content-Type': 'application/json',
},
})
httpClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = tokenService.getLocalAccessToken()
if (token && config.headers)
config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => {
return Promise.reject(error)
},
)
httpClient.interceptors.response.use(
(response) => {
if (response.config.method !== 'get') {
const successMsg = '操作成功完成'
useNotificationsStore().addNotification({ type: 'success', message: successMsg })
}
return response
},
(error) => {
if (error.message === 'Network Error') {
useNotificationsStore().addNotification({ type: 'error', message: '网络错误' })
return Promise.reject(error)
}
let errorMessage = ''
const validationErrors = getValidationErrors(error)
errorMessage = getErrorMessage(error)
useNotificationsStore().addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
}
)
英文:
I managed to solve the problem by reading documentation carefully:
> The easiest way to ensure this is always applied is to defer calls of useStore() by placing them inside functions that will always run after pinia is installed.
I just update http-client by removing useNotificationStore() from the top of the file and where I need notificaitonStore instance use useNotificationStore().addNotification(...)
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'
const httpClient: AxiosInstance
= axios.create({
baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
headers: {
'Content-Type': 'application/json',
},
})
httpClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = tokenService.getLocalAccessToken()
if (token && config.headers)
config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => {
return Promise.reject(error)
},
)
httpClient.interceptors.response.use(
(response) => {
if (response.config.method !== 'get') {
const successMsg = 'operation successfully completed'
useNotificationsStore().addNotification({ type: 'success', message: successMsg })
}
return response
},
(error) => {
if (error.message === 'Network Error') {
useNotificationsStore().addNotification({ type: 'error', message: 'Network Error' })
return Promise.reject(error)
}
let errorMessage = ''
const validationErrors = getValidationErrors(error)
errorMessage = getErrorMessage(error)
useNotificationsStore().addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
}
)
答案2
得分: 0
这是因为您尚未将Pinia与应用程序注册。
```ts
export const createApp = ViteSSG(
App,
{ routes, base: import.meta.env.BASE_URL },
async (ctx) => {
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
.forEach(i => {
i.install?.(ctx)
})
// 安装Pinia
const pinia = createPinia()
ctx.app.use(pinia)
if (import.meta.env.SSR)
ctx.initialState.pinia = pinia.state.value
else
pinia.state.value = ctx.initialState.pinia || {}
// ============
const DEFAULT_TITLE = 'Dashboard'
ctx.router.beforeEach((to) => {
// ====== 使用store ====
const { state } = pinia.useStore()
if (!store.ready)
// 执行(用户实现的)store动作以填充store的状态
store.initialize()
// ===========
if (!state.value.user) {
autoAuthenticate()
}
let title = DEFAULT_TITLE
if (to.meta.title)
title = `${to.meta.title} - ${title}`
document.title = title
})
},
)
英文:
this happens because you have not registered pinia with the app
export const createApp = ViteSSG(
App,
{ routes, base: import.meta.env.BASE_URL },
async (ctx) => {
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
.forEach(i => {
i.install?.(ctx)
})
// install pinia
const pinia = createPinia()
ctx.app.use(pinia)
if (import.meta.env.SSR)
ctx.initialState.pinia = pinia.state.value
else
pinia.state.value = ctx.initialState.pinia || {}
// ============
const DEFAULT_TITLE = 'Dashboard'
ctx.router.beforeEach((to) => {
// ====== use store ====
const { state } = useOidcStore()
if (!store.ready)
// perform the (user-implemented) store action to fill the store's state
store.initialize()
// ===========
if (!state.value.user) {
autoAuthenticate()
}
let title = DEFAULT_TITLE
if (to.meta.title)
title = `${to.meta.title} - ${title}`
document.title = title
})
},
)
> Please remove useOidcStore() on top level
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论