prazdevs / pinia-plugin-persistedstate

💾 Configurable persistence and rehydration of Pinia stores.
https://prazdevs.github.io/pinia-plugin-persistedstate/
MIT License
2.08k stars 120 forks source link

persisting pinia store within Quasar app #46

Closed BenJackGill closed 2 years ago

BenJackGill commented 2 years ago

I'm trying to use this Pinia Plugin in my Quasar app (Vue 3 / TypeScript).

Out of the box everything works fine.

But when using a Quasar boot file the persisted state stops working. Refreshing the page wipes all the new values away.

I don't know why the boot file breaks the persisted state plugin, but I have narrowed the culprit down to a single line...

This is how I am using Pinia with Quasar and adding the plugin:

src/store/index.ts

/* eslint-disable @typescript-eslint/no-unused-vars */
import { store } from 'quasar/wrappers';
import { createPinia, Pinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

declare module '@quasar/app' {
  interface BootFileParams<TState> {
    store: Pinia;
  }
  interface PreFetchOptions<TState> {
    store: Pinia;
  }
}

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: import('pinia').Pinia;
  }
}

export default store(function (_) {
  const pinia = createPinia();
  pinia.use(piniaPluginPersistedstate); // Pinia Plugin added here
  return pinia;
});

And this is what my Pinia store looks like:

src/store/user.ts

import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      user: {
        firstName: 'Mary',
      },
    };
  },
  persist: true, // Note that we are using a persisted state here
  actions: {
    setFirstName(firstName: string) {
      this.user.firstName = firstName;
      console.log('Name set to Pinia store: ', this.user.firstName);
    },
    getFirstName() {
      if (!this.user.firstName) {
        console.log('No name found in store. Setting "John" to Pinia store.');
        this.user.firstName = 'John';
        return this.user.firstName;
      } else {
        console.log('Name fetched from Pinia store: ', this.user.firstName);
        return this.user.firstName;
      }
    },
  },
});

Here is an example front-end page for fetching and setting the firstName:

src/pages/index.vue

<template>
  <div>{{ firstName }}</div>
  <q-form @submit="handleFirstNameSubmit">
    <p>Change First Name</p>
    <q-input v-model="firstNameInput" filled outline />
    <q-btn label="Submit Name to Pinia Store" type="submit" />
  </q-form>
  <q-btn @click="handleFirstNameFetch" label="Fetch Name from Pinia Store" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useUserStore } from 'src/store/user';
const userStore = useUserStore();
const firstName = ref<string>();
const firstNameInput = ref<string>();
const handleFirstNameSubmit = () => {
  if (firstNameInput.value) {
    userStore.setFirstName(firstNameInput.value);
  }
};
const handleFirstNameFetch = () => {
  firstName.value = userStore.getFirstName();
};
</script>

Up to this point everything works fine.

I can set firstName to the Pinia store, refresh the page, and the new name is still in Pinia.

But when trying to use const userStore = useUserStore(store) inside a boot file like the example below, the persisted state stops working:

src/boot/auth.ts

import { boot } from 'quasar/wrappers';
import { useUserStore } from 'src/store/user';

export default boot(({ store }) => {
  const userStore = useUserStore(store);
  // Do some other authentication stuff, setting initial user store values etc, below here...
});

Any idea what's going on? And how to fix it?

I think this plugin is much cleaner than using the alternate LocalStorage persisted state solution so I would love to get it working with Quasar.

prazdevs commented 2 years ago

I dont really see what could be wrong, but I played a bit with the reprod repo, and in the vue devtools, there is no event related to pinia that is fired. This is weird and I dont really understand why, but that could be related to the issue since we subscribe to state mutations (like the devtools). My guess is there is smth with quasar, and i never used it so I can't help much atm. I'll try to look further, or if anyone has an idea.

BenJackGill commented 2 years ago

Ok thanks, you have set me on the right path because I have narrowed it down to being a problem with my Quasar boot file.

Also I realised the opening post wasn't very clear, so I have updated it to reflect the current situation.

Will post an update here if I find one!

yusufkandemir commented 2 years ago

Hi, Quasar core team member here 👋

It's about Pinia's limitation about using plugins before the app is initialized: https://github.com/quasarframework/quasar/discussions/12736#discussioncomment-2338508

We will release first-party Pinia support soon, and mention this limitation on our docs, pointing to the Pinia documentation.

yusufkandemir commented 2 years ago

Quasar Framework has first-party Pinia support since @quasar/app-vite-v1.0.0-beta.1 and @quasar/app-webpack-v3.4.0.

This limitation is by-passed by passing the store to the app instance earlier since @quasar/app-vite-v1.0.0-beta.3 and @quasar/app-webpack-v3.4.3.

So, I think this issue can be closed now. But, I think something like createQuasarPersistedState that uses LocalStorage/SessionStorage and/or Cookies plugins would be really cool. It can be supported with a boot file example(similar to Nuxt's plugins). These great samples from @TobyMosque might be used as a reference implementation:

The boot file that contains an alternative implementation to pinia-plugin-persistedstate: https://github.com/TobyMosque/quasar-v2-ssr-pinia/blob/47d05b5595b42a2c77f5e502f2ffee2b0be76a3d/src/boot/persist-store.ts Individual stores using different storage plugins: https://github.com/TobyMosque/quasar-v2-ssr-pinia/blob/47d05b5595b42a2c77f5e502f2ffee2b0be76a3d/src/stores/persisted-localstorage.ts https://github.com/TobyMosque/quasar-v2-ssr-pinia/blob/47d05b5595b42a2c77f5e502f2ffee2b0be76a3d/src/stores/persisted-sessionstorage.ts https://github.com/TobyMosque/quasar-v2-ssr-pinia/blob/47d05b5595b42a2c77f5e502f2ffee2b0be76a3d/src/stores/persisted-cookie.ts

prazdevs commented 2 years ago

started working on a helper for Quasar, i'll try to expose 3:

👍

TobyMosque commented 2 years ago

localstorage and sessionstorage will work out of box, so u don't need to worry. regarding the cookies, i was able to make Pinia Persisted State Plugin work with the Quasar Cookies Plugin, but my solution is nesty, to not say ugly.

first, u'll need a fake storage:

import { StorageLike } from 'pinia-plugin-persistedstate';

export const cookieStorage: StorageLike = {
  getItem(key: string) {
    return '';
  },
  setItem(key: string, value: string) {
    return;
  },
};

we just need something (the fake storage) to import in the stores, like this:

import { defineStore } from 'pinia';
import { cookieStorage } from './storages';

export const useAppStore = defineStore('app', {
  state: () => ({
    token: '',
  }),
  persist: {
    storage: cookieStorage,
  },
});

so, we can replace that fake storage by the real one in a boot or in the stores/index

import { store } from 'quasar/wrappers';
import { createPinia } from 'pinia';
import PiniaPersistedStatePlugin from 'pinia-plugin-persistedstate';
import { cookieStorage } from './storages';
import { Cookies } from 'quasar';

export default store(({ ssrContext }) => {
  const pinia = createPinia();

  // You can add Pinia plugins here
  // pinia.use(SomePiniaPlugin)

  const cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies; // otherwise we're on client

  pinia.use(({ options }) => {
    if (!options.persist || typeof options.persist === 'boolean') {
      return;
    }
    if (options.persist.storage === cookieStorage) {
      options.persist.storage = {
        getItem(key: string) {
          return JSON.stringify(cookies.get(key));
        },
        setItem(key: string, value: string) {
          const obj = JSON.parse(value);
          cookies.set(key, obj, { path: '/', sameSite: 'Lax', secure: true });
        },
      };
    }
  });
  pinia.use(PiniaPersistedStatePlugin);
  return pinia;
});

This would be done before the PiniaPersistedStatePlugin be configured.

here the source: https://github.com/TobyMosque/ws-auth-samples-frontend/tree/persist-after/src/stores

prazdevs commented 2 years ago

yeah i think i took inspiration from some of your repos as im very unfamiliar with Quasar. Idea was to provide a helper as simple to use as this :

import { boot } from "quasar/wrappers";
import { Cookies } from "quasar";
import { 
  createQuasarCookiesPersistedState 
} from "pinia-plugin-persistedstate/quasar";

export default boot(({ store, ssrContext }) => {
  store.use(createQuasarCookiesPersistedState(Cookies, ssrContext));
});

Kinda like what we did for Nuxt

TobyMosque commented 2 years ago

maybe as a plugin.:

# the package will be named as quasar-app-extension-pinia-persistedstate
quasar ext add pinia-persistedstate

this extension will install the PiniaPersistedStatePlugin, the Cookies Plugins and register the boots. so, in the dev land:

import { defineStore } from 'pinia';
# pinia-plugin-persistedstate/quasar is an alias to quasar-app-extension-pinia-persistedstate
import { CookieStorage, /*LocalStorage, SessionStorage,IndexedStorage*/ } from 'pinia-plugin-persistedstate/quasar';

export const useAppStore = defineStore('app', {
  state: () => ({
    token: '',
  }),
  persist: {
    storage: CookieStorage,
  },
});

more about quasar extensions: https://quasar.dev/app-extensions/tips-and-tricks/inject-quasar-plugin

prazdevs commented 2 years ago

that's fairly interesting. i never use quasar (cause its wayyy too heavy for what i do with vue). But whenever i have time i'll look into maybe making a quasar plugin as you suggest! (or someone can try to do it 😄)

thanks a lot!

TobyMosque commented 2 years ago

cause its wayyy too heavy for what i do with vue.

Fallacies I hear every day ;D Quasar is suitable for all project sizes, even the smallest ones.

prazdevs commented 2 years ago

v2.1.0 now offers quasar helpers. Docs are also updated with how to use it!

I kept it iso with the way the Nuxt helper is made for now.

Closing this for now? feel free to reopen if you feel like something is off/odd/broken

mohamadsdf commented 2 years ago

localstorage and sessionstorage will work out of box, so u don't need to worry. regarding the cookies, i was able to make Pinia Persisted State Plugin work with the Quasar Cookies Plugin, but my solution is nesty, to not say ugly.

first, u'll need a fake storage:

import { StorageLike } from 'pinia-plugin-persistedstate';

export const cookieStorage: StorageLike = {
  getItem(key: string) {
    return '';
  },
  setItem(key: string, value: string) {
    return;
  },
};

we just need something (the fake storage) to import in the stores, like this:

import { defineStore } from 'pinia';
import { cookieStorage } from './storages';

export const useAppStore = defineStore('app', {
  state: () => ({
    token: '',
  }),
  persist: {
    storage: cookieStorage,
  },
});

so, we can replace that fake storage by the real one in a boot or in the stores/index

import { store } from 'quasar/wrappers';
import { createPinia } from 'pinia';
import PiniaPersistedStatePlugin from 'pinia-plugin-persistedstate';
import { cookieStorage } from './storages';
import { Cookies } from 'quasar';

export default store(({ ssrContext }) => {
  const pinia = createPinia();

  // You can add Pinia plugins here
  // pinia.use(SomePiniaPlugin)

  const cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies; // otherwise we're on client

  pinia.use(({ options }) => {
    if (!options.persist || typeof options.persist === 'boolean') {
      return;
    }
    if (options.persist.storage === cookieStorage) {
      options.persist.storage = {
        getItem(key: string) {
          return JSON.stringify(cookies.get(key));
        },
        setItem(key: string, value: string) {
          const obj = JSON.parse(value);
          cookies.set(key, obj, { path: '/', sameSite: 'Lax', secure: true });
        },
      };
    }
  });
  pinia.use(PiniaPersistedStatePlugin);
  return pinia;
});

This would be done before the PiniaPersistedStatePlugin be configured.

here the source: https://github.com/TobyMosque/ws-auth-samples-frontend/tree/persist-after/src/stores

thanks aloooot man <3