prazdevs / pinia-plugin-persistedstate

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

[nuxt] Pinia State doesn't persist when set from middleware #221

Open d0peCode opened 12 months ago

d0peCode commented 12 months ago

Describe the bug

This is my Pinia module:

import { defineStore } from "pinia";
export const useStore = defineStore("store", {
    state: () => ({
        token: '',
    }),

    actions: {
        setToken(token: string) {
            this.token = token;
        }
    },

    persist: true
});

I set the state with setToken function from the middleware

import {useStore} from "~/store/useStore";

export default defineNuxtRouteMiddleware(() => {
    const store = useStore();
    console.log('store.token1', store.token)
    store.setToken('token')
    console.log('store.token2', store.token)
});

Now on first app reload I would expect to see logs:

store.token1
store.token2 token

and on the second reload I would expect to see

store.token1 token store.token2 token

Instead I see:

image

Reproduction

https://github.com/d0peCode/nuxt3-pinia-middleware-issue

System Info

MacOS, Chrome

Used Package Manager

npm

Validations

MZ-Dlovely commented 12 months ago

maybe you're reading log from your terminal? when server render, console.log while print at terminal and there is no localstorage. may you can watch the console on chrome

d0peCode commented 12 months ago

maybe you're reading log from your terminal? when server render, console.log while print at terminal and there is no localstorage. may you can watch the console on chrome

Doesn't it work with cookies? I thought it is SSR friendly and I can read persisted values from store in my middleware.

MZ-Dlovely commented 12 months ago

maybe you're reading log from your terminal? when server render, console.log while print at terminal and there is no localstorage. may you can watch the console on chrome

Doesn't it work with cookies? I thought it is SSR friendly and I can read persisted values from store in my middleware.

when first run app and enter page, this route middleware will trigger immediately. store will be Initialized and token will be changed. let's see our plugin. it ready to use storage, but it will check useNuxtApp().ssrContext which is not instantiated this time. so nuxt say A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

MZ-Dlovely commented 12 months ago

you can try to run getCurrentInstance() in route middleware and print its result. useNuxtApp mainly obtain nuxt app through getCurrentInstance()?.appContext.app.$nuxt

d0peCode commented 12 months ago

I'm changing the state of pinia module from the middleware which suppose to have access to nuxt instance.

I thought that the change to pinia store which I made from the middleware would persist with your plugin.

From middleware I execute function which change pinia store. Cookie is not created by pinia-plugin-persistedstate. Look at reproduction repository.

MZ-Dlovely commented 12 months ago

maybe we can change the judgment method, like:

function usePersistedstateSessionStorage() {
  return ({
    getItem: (key) => {
      return checkWindowsKey('sessionStorage')
        ? sessionStorage.getItem(key)
        : null
    },
    setItem: (key, value) => {
      if (checkWindowsKey('sessionStorage'))
        sessionStorage.setItem(key, value)
    },
  }) as StorageLike
}

function checkWindowsKey(key: string) {
  return process.dev && key in window
}

how do you think about it? @prazdevs

d0peCode commented 12 months ago

maybe we can change the judgment method, like:

function usePersistedstateSessionStorage() {
  return ({
    getItem: (key) => {
      return checkWindowsKey('sessionStorage')
        ? sessionStorage.getItem(key)
        : null
    },
    setItem: (key, value) => {
      if (checkWindowsKey('sessionStorage'))
        sessionStorage.setItem(key, value)
    },
  }) as StorageLike
}

function checkWindowsKey(key: string) {
  return process.dev && key in window
}

how do you think about it? @prazdevs

I thought Cookies is default but even when I changed my store to:

import { defineStore } from "pinia";
export const useStore = defineStore("store", {
    state: () => ({
        token: '',
    }),

    actions: {
        setToken(token: string) {
            this.token = token;
        }
    },

    persist: {
        storage: persistedState.cookiesWithOptions({
            sameSite: 'strict',
        }),
    },
});

It also doesn't work:

image

EDIT: sorry I accidentally pasted wrong image previously thus edit

d0peCode commented 12 months ago

Also cookie is not created:

image
MZ-Dlovely commented 12 months ago

whether it's localstorage or cookie, it will all use useNuxtApp. you can use object like { debug: true } to replace true, then you can see the stacks about the error.I think I described the reason for the mistake in the previous two consecutive comments.

d0peCode commented 12 months ago

So your plugin doesn't make pinia state persist if you set state from the server side of nuxt app lifecycle?

MZ-Dlovely commented 11 months ago

sure, if you change the state at server side, how we know you have changed when we stand on client side. about share data between server and client, nuxt3 suggests using useState. of course, we can require owner of plugin to improve and perfect persistedState. it may be support to use useState.

MZ-Dlovely commented 11 months ago

if you wang to persist at client storage after server side changed, we can try to do. but if you want to read value from client storage when server render, it's impossible, because there is no connection to the client side even though itself nuxt3.

Ena-Heleneto commented 11 months ago

you can do this

export default defineNuxtRouteMiddleware(to => {
  // skip middleware on server
  if (process.server) return
  // skip middleware on client side entirely
  if (process.client) return
  // or only skip middleware on initial client load
  const nuxtApp = useNuxtApp()
  if (process.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return
})

https://nuxt.com/docs/guide/directory-structure/middleware

d0peCode commented 11 months ago

sure, if you change the state at server side, how we know you have changed when we stand on client side. about share data between server and client, nuxt3 suggests using useState. of course, we can require owner of plugin to improve and perfect persistedState. it may be support to use useState.

You could check the changes in pinia in nuxt server side hooks and create server side cookie to then hydrate on client. There is many possibilities to achieve real SSR-friendly persistent state.

If you don't support making changes in pinia store from the server side and don't persist those changes then [pinia-plugin-persistedstate] is not SSR friendly and you should mention it in docs

MZ-Dlovely commented 11 months ago

You could check the changes in pinia in nuxt server side hooks and create server side cookie to then hydrate on client. There is many possibilities to achieve real SSR-friendly persistent state.

If you don't support making changes in pinia store from the server side and don't persist those changes then [pinia-plugin-persistedstate] is not SSR friendly and you should mention it in docs

if you think we are not SSR-friendly because of we cannot persist client data on server side, just like asking me to count how many lights there are in your house. when you first visited me, I didn't even know who you were, let alone let me find your house.

d0peCode commented 11 months ago

if you think we are not SSR-friendly because of we cannot persist client data on server side

Your library is working on client side only. With Nuxt you can load any javascript package on client side only. Going with your logic everything anyone has ever wrote in javascript is SSR friendly. :)

SSR in Nuxt gives you entire server side lifecycle. You could hook up function at the end of server lifecycle just before app is sent to browser. In this function you could create a cookie using h3 library with current pinia state.

Then in the browser you could read this cookie, compare and detect changes and apply them to the pinia.

This way every change you've made on server to your pinia module - in server plugin, middleware or whatever - would persist and your library would be SSR friendly.

prazdevs commented 11 months ago

PRs are always welcome :)

That being said, keep in mind the nuxt module is an implementation of the base plugin, to work easily with Nuxt, and for most uses. Not everyone uses middleware and modify pinia stores in there.

So, yes, the library is SSR friendly with most use cases.

There are lots of cases that could be improved on the nuxt part, but the nuxt implementation is very simple. Keep in mind most of it is my work, on my free time, over nights, so I'd ask to stay respectful, for me and everyone who has contributed so far.

SSR is a very complex topic, and Nuxt still changes a lot. Keeping server and client in sync is ridiculously difficult, let alone middleware or server components... The Nuxt module was created when Nuxt3 was released officially, and docs were not even complete. Nuxt module docs are still not complete, and unit testing is still in RFC!

tl;dr: be respectful. the module fulfils most people's needs (ssr included) and there will be improvement eventually.

also thanks @Abernethy-BY & @MZ-Dlovely for the answers 👍

MZ-Dlovely commented 11 months ago

I'm sorry that my thought is wrong before you explained. but the good news is that I have an idea, and I'm trying to solve it tomorrow.

MZ-Dlovely commented 11 months ago

I have done a lot of stupid things. :( after trying several possibilities, I found that just using it store.$persist() is enough. just like your code @d0peCode :

import {useStore} from "~/store/useStore";

export default defineNuxtRouteMiddleware(() => {
    const store = useStore();
    console.log('store.token1', store.token)
    store.setToken('token')
    console.log('store.token2', store.token)
    // just do this
    preset_cookie.$persist()
});
MZ-Dlovely commented 11 months ago

if you dont wang to use it by yourself, i can write some thing to make it automatic

erdemyunsel commented 7 months ago

Same on me. I cant get persist states in middleware after refresh page.

import { useMainStore } from "~/store";

export default defineNuxtRouteMiddleware(async (to, from) => {
  const nuxtApp = useNuxtApp()

  const store =  useMainStore();
  const config = useRuntimeConfig();

  const authRequiredPages = ["/panel", "/teklif-al"];
  // ? if user is not logged in and to.path.startsWith authRequiredPages
  const isLoginRequired = authRequiredPages.some((authPaths) => {
    if (to.path.startsWith(authPaths)) {
      return true;
    }
  });

  if (isLoginRequired) {
    const { data: sessionControl } = await useFetch(
      `${config.public.API_URL}users/session`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${store.token}`,
        },
      }
    );
    if (sessionControl?.value?.user) {
      store.setUser(sessionControl.value.user);
      store.setToken(sessionControl.value.token);
    } else {
      store.logout();
      return navigateTo("/giris");
    }
  }else if(isLoginRequired && !store.token){
    return navigateTo("/giris");
  }

});
MZ-Dlovely commented 7 months ago

yep!(clap hands

erdemyunsel commented 7 months ago

Now its okay with adding middleware to this.

  if (process.server) {
    return
  }

Now middleware.


...
export default defineNuxtRouteMiddleware(async (to, from) => {
  const nuxtApp = useNuxtApp()

  const store =  useMainStore();
  const config = useRuntimeConfig();

  if (process.server) {
    return
  }

  const authRequiredPages = ["/panel", "/teklif-al"];
  // ? if user is not logged in and to.path.startsWith authRequiredPages
  const isLoginRequired = authRequiredPages.some((authPaths) => {
    if (to.path.startsWith(authPaths)) {
      return true;
    }
  });

....
RomainMazB commented 6 months ago

I have done a lot of stupid things. :( after trying several possibilities, I found that just using it store.$persist() is enough. just like your code @d0peCode :

import {useStore} from "~/store/useStore";

export default defineNuxtRouteMiddleware(() => {
    const store = useStore();
    console.log('store.token1', store.token)
    store.setToken('token')
    console.log('store.token2', store.token)
    // just do this
    preset_cookie.$persist()
});

This was the missing piece, thanks!!

I'm using a check-auth.global.ts middleware to refresh and store the session token and faced this issue as well.

Just adding the $persist method after refreshing the token fixed my issue.

localusercamp commented 2 weeks ago

This does not work in nuxt SSR $fetch and useFetch interseptors (onResponse(), onResponseError() and so on) as well... I just can not clear auth store value when getting 401.

async onResponseError({ options, response }) {
  if (response.status === HttpStatusCode.Unauthorized) {
    console.log('Unauthorized.');

    const store = useAuthStore();

    store.$reset();
    store.$persist();
    // Store changes here but on client nothing changes
  }
},
RomainMazB commented 6 days ago

@localusercamp I think that your issue here is that the interceptors are ran outside of the NuxtApp scope. You probably get a warning in the SSR console like

A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function

Something like this should work:

// composables/useAPI.ts
export default () => {
  // Declare the store from within the composable scope
  const store = useAuthStore()

  // V2: If the pinia state isn't retrieved, you can retrieve it and pass it to the store
  // const {$pinia} = useNuxtApp()
  // const store = useAuthStore($pinia)

  function myUseFetch(url, options) {
    options.onResponseError = async ({ options, response }) => {
        if (response.status === HttpStatusCode.Unauthorized) {
          console.log('Unauthorized.');

          // Consume it from the interceptor callback
          store.$reset();
          store.$persist();
        }
    }

    return useFetch(url, options)
  }

  return { myUseFetch }
}