freddy38510 / quasar-app-extension-ssg

Static Site Generator App Extension for Quasar.
MIT License
153 stars 16 forks source link

pinia persisted state: storages (session storage) #356

Open tamysiby opened 1 year ago

tamysiby commented 1 year ago

Hi, thanks so much for this project! I manage to get most of my project running, but I can't seem to manage to use pinia-plugin-persistedstate and its sessionStorage without getting hacky. Is there any config part that I missed out on or is that something in progress?

I've added if (typeof window !== 'undefined') pinia.use(piniaPluginPersistedstate) in the store config, and added { persist: typeof window === 'undefined' ? false : { storage: sessionStorage, }, } in my individual stores to make sure they're running in the client side. Despite causing glitches, I guess it works. Just wondered if there are configs for pinia or rehydration. Thank you again!

freddy38510 commented 1 year ago

I've played around with this plugin a few days ago.

Instead of using type of window !== 'undefined', you can use !import.meta.env.SSR or process.env.CLIENT. It will be statically replaced by true or false when the app is compiled, allowing tree-shaking of unreachable code.

When a pre-rendered page is fully loaded by the browser, the store is primed with the server-initialized state. But when navigating to subsequent pages (you are in SPA mode now), there is no server-initialized state.

Edit: This part of the comment was irrelevant.

Here's how I managed to take both scenarios into account:

// src/stores/index.js

import { store as createStore } from 'quasar/wrappers';
import { createPinia } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';

export default createStore((/* { ssrContext } */) => {
  const pinia = createPinia();

  if (!import.meta.env.SSR) {
    pinia.use(createPersistedState({
      auto: true,
      beforeRestore: ({ store }) => {
        if (store.$state.hasChanged === true) {
          store.$persist();
        }
      },
    }));
  }

  return pinia;
});
// src/stores/example.js

import { defineStore } from 'pinia';

export const useExampleStore = defineStore('example', {
  state: () => ({
    counter: 0,
    data: null,
    hasChanged: false,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
  actions: {
    increment() {
      this.counter += 1;
      this.hasChanged = true;
    },
    async fetchData() {
      this.data = await (await fetch('https://someapi.com')).json();
      this.hasChanged = true;
    },
  },
});
// src/components/ExampleComponent.vue

<template>
  <div>
    <div>Direct store</div>
    <!-- Read the state value directly -->
    <div>{{ store.counter }}</div>
    <pre>{{ store.data }}</pre>
    <!-- Use getter directly -->
    <div>{{ store.doubleCount }}</div>

    <!-- Manipulate state directly -->
    <q-btn @click="store.counter--">-</q-btn>
    <!-- Use an action -->
    <q-btn @click="store.increment()">+</q-btn>
  </div>

</template>

<script>
import { useExampleStore } from 'stores/example';
import { onBeforeMount, onServerPrefetch } from 'vue';

export default {
  setup() {
    const store = useExampleStore();

    // this hook is executed at server side only
    onServerPrefetch(async () => {
      await store.fetchData();
    });

    // this hook is executed at client-side only
    onBeforeMount(async () => {
      if (store.hasChanged === false) {
        await store.fetchData();
      }
    });

    return {
      store,
    };
  },
};
</script>

freddy38510 commented 1 year ago

The use of ssg/ssr and client-side persistent state leads to two distinct sources of state.

You have one source initialized at build-time, and an other from a storage at client-side.

If these two sources differ, there will be a client-side warning about the hydration mismatch.

You can enable the Quasar manualStoreHydration option, but you'll still have hydration mismatch. This is because the pre-rendered content will have the initial state, which may differ from the persistent state.

There are two ways of solving this problem:

The disadvantage of these two solutions is that you'll have to manage a kind of loading state, rather than having content that changes abruptly.

For the second solution, I found a not-so-clean way of doing it by using the 'onMounted' hook in the root component "App.vue":

// src/App.vue

<template>
  <router-view />
</template>

<script setup>
import { getActivePinia } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';
import { onMounted } from 'vue';

if (process.env.CLIENT) {
  const pinia = getActivePinia();

  pinia?.use((context) => {
    onMounted(() => {
      createPersistedState({
        auto: true,
        beforeRestore: () => {
          console.log('beforeRestore');
        },
        afterRestore: () => {
          console.log('afterRestore');
        },
      })(context);
    });
  });
}
</script>
// src/store/index.js

import { store as createStore } from 'quasar/wrappers';
import { createPinia } from 'pinia';

export default createStore(() => createPinia());