8

I'm building a Vue 3 app using the OptionsAPI along with a Pinia Store but I frequently run into an issue stating that I'm trying to access the store before createPinia() is called.

I've been following the documentation to use the Pinia store outside components as well, but maybe I'm not doing something the proper way.

Situation is as follows:

I have a login screen (/login) where I have a Cognito session manager, I click a link, go through Cognito's signup process, and then get redirected to a home route (/), in this route I also have a subroute that shows a Dashboard component where I make an API call.

On the Home component I call the store using useMainStore() and then update the state with information that came on the URL once I got redirected from Cognito, and then I want to use some of the state information in the API calls inside Dashboard.

This is my Home component, which works fine by itself, due to having const store = useMainStore(); inside the mounted() hook which I imagine is always called after the Pinia instance is created.

<template>
  <div class="home">
    <router-view></router-view>
  </div>
</template>

<script>
import {useMainStore} from '../store/index'

export default {
  name: 'Home',
  components: {
  },
  mounted() {
    const store = useMainStore();

    const paramValues = {}

    const payload = {
      // I construct an object with the properties I need from paramValues
    }

    store.updateTokens(payload); // I save the values in the store
  },
}
</script>

Now this is my Dashboard component:

<script>
import axios from 'axios'
import {useMainStore} from '../store/index'

const store = useMainStore();

export default {
    name: "Dashboard",
    data() {
    return {
        user_data: null,
      }
  },
  mounted() {
    axios({
      url: 'myAPIUrl',
      headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
    }).then(response => {
      this.user_data = response.data;
    }).catch(error => {
      console.log(error);
    })
  },
}
</script>

The above component will fail, and throw an error stating that I'm trying to access the store before the instance is created, I can solve this just by moving the store declaration inside the mounted() hook as before, but what if I want to use the store in other ways inside the component and not just in the mounted hook? And also, why is this failing? By this point, since the Home component already had access to the store, shouldn't the Dashboard component, which is inside a child route inside Home have the store instance already created?

This is my main.js file where I call the createPinia() method.

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const pinia = createPinia();

createApp(App).use(router).use(pinia).mount('#app')

And the error I get is:

Uncaught Error: []: getActivePinia was called with no active Pinia. Did you forget to install pinia?

My Store file:

import { defineStore } from 'pinia';

export const useMainStore = defineStore('main', {
  state: () => ({
    access_token: sessionStorage.getItem('access_token') || '',
    id_token: sessionStorage.getItem('id_token') || '',
    token_type: sessionStorage.getItem('token_type') || '',
    isAuthenticated: sessionStorage.getItem('isAuthenticated') || false,
    userData: JSON.parse(sessionStorage.getItem('userData')) || undefined
  }),
  actions: {
    updateTokens(payload) {
      this.id_token = payload.id_token;
      this.access_token = payload.access_token;
      this.token_type = payload.token_type

      sessionStorage.setItem('id_token', payload.id_token);
      sessionStorage.setItem('access_token', payload.access_token);
      sessionStorage.setItem('token_type', payload.token_type);
      sessionStorage.setItem('isAuthenticated', payload.isAuthenticated);
    },
    setUserData(payload) {
      this.userData = payload;
      sessionStorage.setItem('userData', JSON.stringify(payload));
    },
    resetState() {
      this.$reset();
    }
  },
})
IvanS95
  • 5,364
  • 4
  • 24
  • 62
  • You didn't specify which Vue version you use, which is important. It's not a typical usage to use use... functions outside a component. "By this point, since the Home component already had access to the store, shouldn't the Dashboard component, which is inside a child route inside Home have the store instance already created?" - this isn't how lifecycle works. Dashboard evaluates useMainStore when the module is imported which likely happens before pinia plugin is installed. You can see the order yourself by placing breakpoints. – Estus Flask Feb 17 '22 at 19:08
  • @EstusFlask I'm using Vue 3, and fair enough, I'm trying to understand how to make sure I use the store only after Pinia is installed, I'm calling the `createPinia()` method in my `main.js` file where my app is mounted – IvanS95 Feb 17 '22 at 19:13
  • Please, update the question with actual error, it will help other users who have the same problem – Estus Flask Feb 17 '22 at 19:24
  • I'll post the answer. Can you also show '../store/index' for completeness? – Estus Flask Feb 17 '22 at 20:01
  • @EstusFlask I added it now – IvanS95 Feb 17 '22 at 20:08

3 Answers3

3

It's possible but not common and not always allowed to use use composition functions outside a component. A function can rely on component instance or a specific order of execution, and current problem can happen when it's not respected.

It's necessary to create Pinia instance before it can be used. const store = useMainStore() is evaluated when Dashboard.vue is imported, which always happen before createPinia().

In case of options API it can be assigned as a part of component instance (Vue 3 only):

  data() {
    return { store: useMainStore() }
  },

Or exposed as global property (Vue 3 only):

const pinia = createPinia();
const app = createApp(App).use(router).use(pinia);
app.config.globalProperties.mainStore = useMainStore();
app.mount('#app');
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • So, just to clarify, I should avoid calling `useMainStore()` outside of any component instance, always either inside the `data()` function or other methods or hooks? – IvanS95 Feb 22 '22 at 16:29
  • 1
    Yes. Composable "use" functions are intended to be used directly in setup function but can also work in `data` because it's executed at the same time as `setup`. – Estus Flask Feb 22 '22 at 16:37
  • Praise the Sun! Thanks, I'll make the changes to my app – IvanS95 Feb 22 '22 at 16:38
  • "Possible" but "not always allowed", so not always possible? – Lee Goddard Aug 30 '22 at 07:35
  • @LeeGoddard Possible for some composable functions but not others. This is determined by the actual implementation of a composable and the place where it's used. If a function happens to run during a setup because it's indirectly called at that time, it's allowed – Estus Flask Aug 30 '22 at 08:34
  • @EstusFlask Indeed. Worth adding to the answer? – Lee Goddard Aug 30 '22 at 09:33
  • the pinia documentation is horrendous. – qodeninja Jun 05 '23 at 21:42
2

Since you're using Vue 3, I suggest you to use the new script setup syntax:

<script setup>
    import { reactive, onMounted } from 'vue'
    import axios from 'axios'
    import { useMainStore } from '../store'
    
    const store = useMainStore();
    
    const data = reactive({
       user_data: null
    })        
     
    onMounted (async () => {
      try {
        const {data: MyResponse} = await axios({
          method: "YOUR METHOD",
          url: 'myAPIUrl',
          headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
        })
        
        data.user_data = MyResponse

      } catch(error){
            console.log(error)
        }
    })

</script>

Using setup you can define that store variable and use it through your code.

thidzz
  • 173
  • 1
  • 3
  • 9
  • Thank you, I was looking for a solution with the Options API – IvanS95 Feb 17 '22 at 19:38
  • Just to add, if you use Vetur extension, vscode will complain about that syntax (I checked this months ago, I don't know about now). Vue developers suggest using "Volar" extension for that matter. – thidzz Feb 17 '22 at 20:58
0

everyone after a lot of research I found the answer to this issue, you must pass index.ts/js for const like below:

<script lang="ts" setup>
import store from '../stores/index';
import { useCounterStore } from '../stores/counter';

const counterStore = useCounterStore(store());
counterStore.increment();
console.log(counterStore.count);
</script>
Pirooz Jenabi
  • 440
  • 5
  • 7