In Vue-js 3 if you are using Pinia state management sometimes we may need to use the store externally outside of components.
Pinia store is the latest store used alongside latest version of vuejs which is Vue 3. The other alternative we usually use is Vuex as the state management but the ease of Pinia store compared with Vuex makes it the first choice for new projects.
Regarding Pinia stores in this article let’s see how to use Pinia outside of components. Note that the snippets shown in the article i described them in the context of Vue 3 composition Api.
In a typical Vue 3 app usually we load our Pinia stores inside of setup or a composable file:
<script setup> import { useUserStore } from '@/stores/user' // access the `store` variable anywhere in the component const store = useUserStore() </script>
And you can access the store state, actions and getters inside of setup or template. In this code this works out of the box because Pinia instance automatically injected and identified inside of the component.
But when thinking about using the store in external js file things can be a little tricky.
To describe this i will create a simple example. In our example i will assume that we have multiple pages in which some pages is protected by some kind or authentication mechanism.
Go ahead and create Vue 3 app with vite:
npm create vite@latest vue_pinia_store_outside -- --template vue
Check the proper command when creating Vue apps with Vite, in my case my npm version > 7 so i have to use the above command.
For npm 6.x:
npm create vite@latest vue_pinia_store_outside --template vue
Next install Pinia library:
npm install pinia
Also install vue router v4:
npm install vue-router@4
Next open the project in your IDE, we will create some files.
Inside of src/components/ create these components:
Home.vue
<script setup> const title = 'Home'; </script> <template> <h1>This is {{ title }} page</h1> </template>
Profile.vue
<script setup> const title = 'Profile'; </script> <template> <h1>This is {{ title }} page</h1> <p>This is a protected page</p> </template>
The Profile.vue page supposed to be a protected page so we will add the logic to allow access to authenticated users only.
Then add a routes.js file for our pages:
src/routes.js
import {createWebHistory, createRouter} from "vue-router"; import Home from "./components/Home.vue"; import Profile from "./components/Profile.vue"; const routes = [ { path: '/', component: Home }, { path: '/profile', component: Profile, meta: {requiresAuth: true} } ]; const router = createRouter({ history: createWebHistory(), routes, // short for `routes: routes` }); export {router};
In this code i declared two routes for home and profile pages. In the profile route object i added a meta key requiresAuth that will be used later on to prevent access to this page for unauthenticated users.
Apply the routes by modifying main.js file
src/main.js
import { createApp } from 'vue' import { createPinia } from 'Pinia'; import './style.css' import App from './App.vue' import {router} from "./routes"; const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(router) app.mount('#app')
In this code i imported the router we just created in routes.js then update the call to createApp() function and applied the router using app.use() function. If you notice that i imported applied Pinia using createPinia() to our Vue app.
One more thing is to update App.vue component like shown:
src/App.vue
<script setup> </script> <template> <header> <nav> <ul> <li> <RouterLink to="/">Home</RouterLink> </li> <li> <RouterLink to="/profile">Profile</RouterLink> </li> </ul> </nav> </header> <router-view></router-view> </template> <style scoped> header nav ul { list-style-type: none; } header nav ul li { display: inline; margin-inline-start: 5px; } </style>
Then launch the app:
npm run dev
After running the app you can see in the browser the two pages we just created and you can navigate between to each page.
Let’s create a simple Pinia store that stores the authentication status of the user.
src/store/useAuth.js
import { defineStore } from "pinia"; import { computed, ref } from "vue"; export const useAuthStore = defineStore('auth', () => { const authenticated = ref(false); return { authenticated } });
This is a setup store useAuthStore created with the defineStore() pinia function that accepts the store Id and callback function that contains the store state, getters and actions.
For simplicity the store contains just state variable authenticated with a boolean value that indicates if user authenticated or not.
To get to our point in this article we need to apply the store. However i will not apply it in a component rather i will be using it in routes.js file.
If you remember in the above route, the profile route has a requiresAuth meta key to disable access if are authenticated.
So we will be using the router navigation guard. Add this code before export {router} statement like so:
routes.js
..... ..... import { useAuthStore } from "./store/useAuth"; .... .... .... const store = useAuthStore(); router.beforeEach((to, from, next) => { // redirect to home if user have no access if(to.meta.requiresAuth && !store.authenticated) return next('/'); next(); });
The beforeEach is a global router navigation guard, triggered globally for each route before going to specific page. This is a suitable place to add custom logic to continue navigation or stop navigation using the next() function.
In this code i am checking if the meta requiresAuth for the incoming route is true and i am not authenticated then i go back to the home screen otherwise i continue.
Unfortunately if you return back to the browser and refresh the app will not work and you can see an error in the browser console like this error:
Uncaught Error: "getActivePinia()" was called but there was no active Pinia
The reason for this error is that you invoked useAuthStore() but Pinia instance not yet injected into Vue app and thereby Vue cannot identify this as a Pinia store.
To make this to work you have to move the useAuthStore() call inside of beforeEach() hook like so:
router.beforeEach((to, from, next) => { const store = useAuthStore(); // redirect to home if user have no access if(to.meta.requiresAuth && !store.authenticated) return next('/'); next(); });
Now refresh the browser again you will see the app is working. Of course when clicking on the profile link you see page not redirecting because the state variable authenticated in the store is false.
Revert it back to true and you can navigate to profile screen:
const authenticated = ref(true);
The app works, but why? Because in the beforeEach() hook the Vue app is already loaded and initialized the Pinia instance.
From this we can understand that when using Pinia stores in external files we have to keep track of the order of loading Pinia instance and whether it’s loaded or not, for example we can load the store in main.js like so:
import { createApp } from 'vue' import { createPinia } from 'pinia'; import './style.css' import App from './App.vue' import {router} from "./routes"; import { useAuthStore } from './store/useAuth'; const app = createApp(App) const pinia = createPinia(); app.use(pinia); app.use(router) const authStore = useAuthStore(); // This line is correct and works fine app.mount('#app')
Here this code is working because the order is correct, first i load Vue using createApp(), then created Pinia using createPinia() function then invoking useAuthStore().
But when changing the order by invoking useAuthStore() prior to createPinia() it won’t work.
..... ..... const authStore = useAuthStore(); // This will fail because it preceeds createPinia() call const app = createApp(App) const pinia = createPinia(); app.use(pinia); app.use(router) app.mount('#app')
SSR Apps
For SSR (Server Side Rendering) Apps to use Pinia store in the setup based components you can just invoke your store normally:
<script setup> // this works because pinia knows what application is running inside of setup const auth = useAuthStore() </script>
However in the options Api based components when calling a pinia store you need to provide the pinia instance to the useStore function:
serverPrefetch() { const auth = useAuthStore(this.$pinia) }
As you see can access the pinia instance using this.$pinia