In a Pinia single-page app, why does calling useAuthStore() at the top of the router module fail, and where should the call go?
answer
- import time versus run time
- where useStore finds its pinia
- app.use(pinia) sets the active one
- no active Pinia error
- move the call into the guard
basics
~10 sA store call needs a pinia; module-level code runs at import, before app.use(pinia) makes one active, so Pinia throws a no-active-Pinia error in development. Call useAuthStore() inside the guard function, which runs after installation.
solid answer
~40 s`useAuthStore()` has to find a pinia: first one passed as an argument, then one it can `inject()` because it runs inside a component or an app context, and finally the module-level **active pinia** that `app.use(pinia)` sets during installation. A call at the top of `router/index.ts` runs when the module is imported, typically before `main.ts` reaches `app.use(pinia)`, so none of the three exists. In development Pinia throws `"getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?`; in production the code fails on an undefined pinia. The fix is to defer the call: put `const auth = useAuthStore()` inside the `beforeEach` callback, which only runs during navigation, after both plugins are installed.
code
ts · 15 linesimport { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { routes } from './routes'
export const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})go deeper
Recall that a Pinia store call needs app.use(pinia) to have run first, and that router guards are a safe place to call it.
Explain the lookup order (argument, injection, active pinia) and why code at module scope runs at import time, before any of them exists.
Spot import-order bugs that only appear in some builds, and explain why the single-page fallback to the active pinia is not safe under server rendering.
Set a codebase rule that stores are only called inside functions that run after installation, and enforce it in review or linting.
## How useStore() finds its pinia A function returned by `defineStore()` (here `useAuthStore`) does not own any state. It looks up the **pinia** instance, the container created by `createPinia()`, and asks it for the store with that id, creating the store on first use. It resolves the pinia in a fixed order: 1. **An explicit argument**: `useAuthStore(pinia)`. 2. **Injection**: if `hasInjectionContext()` is true (inside a component's `setup()`, or inside `app.runWithContext()`), it calls `inject()` to get the pinia provided by `app.use(pinia)`. 3. **The active pinia**: a module-level variable that `app.use(pinia)` sets during installation and that every successful lookup refreshes. If all three are missing, a development build throws: > "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"? ... This will fail in production. A production build skips the check and fails a line later, reading properties of an undefined pinia. ## Why the router module hits it ```ts import { useAuthStore } from '@/stores/auth' const auth = useAuthStore() export const router = createRouter({ /* ... */ }) ``` That top-level line runs **when the module is imported**. `main.ts` imports the router module near the top, before it calls `createPinia()` and `app.use(pinia)`, so at that moment there is no argument, no injection context and no active pinia. Whether it fails can even depend on import order: if some other path happens to install Pinia first, the same line works, which makes the bug look random. ## The fix: defer the call Move the call into code that runs after installation. A navigation guard is ideal, because the router starts navigating only once it is installed, and its guards run asynchronously, after `main.ts` has also installed Pinia: ```ts router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { return { path: '/login', query: { redirect: to.fullPath } } } }) ``` The store is created on the first navigation and cached in the pinia, so calling `useAuthStore()` on every navigation is cheap: later calls return the same store. Other places that are safe for the same reason: - inside actions and getters of another store; - inside event handlers, timers and callbacks that run after the app is mounted; - inside functions exported from a service module, as long as they are called later rather than at import. ## Where each call site lands | Call site | Pinia found via | Works in a single-page app? | |---|---|---| | top of a module, before `app.use(pinia)` | nothing | no: throws in development | | `<script setup>` of a component | injection | yes | | inside `router.beforeEach()` | injection (the router runs guards in the app's context) | yes | | after `app.use(pinia)` in `main.ts` | active pinia | yes | In a single-page app, once `app.use(pinia)` has run, even a call with no injection context works, because the active pinia is set and there is only one. Server rendering changes that, because many requests share one server process; the rules there are stricter. ## Diagnosing it in an existing app When the error appears in a codebase you did not write, a few checks locate it quickly: 1. Read the stack trace: the failing frame is a module's top level (often a router, an API client or a composable file), not a component. 2. Open `main.ts` and note the order of imports versus `createPinia()` and `app.use(pinia)`; everything imported above them runs first. 3. Search for store calls that sit outside any function body, including default parameter values and module-level constants. 4. If the error only appears in one build or one entry point, compare the import order between them; a lazy-loaded route can hide the problem in one and expose it in another. The same message appears in unit tests that call a store without an active pinia; the testing helpers exist for that case. ## What not to do - Do not work around it by moving `createPinia()` into the router module or calling `setActivePinia()` by hand in app code; that hides the ordering problem and creates a second source of truth. - Do not cache the store in a module-level variable filled lazily "the first time", which reintroduces a global. ## Common mistakes - Believing the store is created when `defineStore()` runs; it is created on the first `useStore()` call. - Assuming the production build behaves like development and only logs a warning. - Calling the store at module scope because it "worked on my machine" with a different import order.
- Is it expensive to call useAuthStore() on every navigation instead of once at module level?No. Only the first call creates the store; the pinia caches it by id, so every later call returns the same reactive store object. The cost is a lookup, not a new store, and the state is shared across all callers.
- Why does useAuthStore() inside a Vue Router 5 guard find the pinia through injection rather than the global fallback?Vue Router runs each guard inside `app.runWithContext()` of the app it is installed in. That makes `hasInjectionContext()` true for the synchronous part of the guard, so `useAuthStore()` injects the app's pinia directly, which is also what keeps guards correct under server rendering.
saying these in an interview costs you the question
- defineStore() creates the store as soon as the module is imported
- The no-active-Pinia error is only a development warning, production works
- Calling the store on every navigation creates a new store each time
- Calling setActivePinia() in the router module is the proper fix
- Stores can only be used inside components, never in router guards