pinia-plugin-persistedstate
pinia-plugin-persistedstate copied to clipboard
[nuxt] Pinia State doesn't persist when set from middleware
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:
Reproduction
https://github.com/d0peCode/nuxt3-pinia-middleware-issue
System Info
MacOS, Chrome
Used Package Manager
npm
Validations
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guide.
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [X] The provided reproduction is a minimal reproducible of the bug.
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
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.
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.
you can try to run getCurrentInstance()
in route middleware and print its result. useNuxtApp
mainly obtain nuxt app through getCurrentInstance()?.appContext.app.$nuxt
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.
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
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:
EDIT: sorry I accidentally pasted wrong image previously thus edit
Also cookie is not created:
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.
So your plugin doesn't make pinia state persist if you set state from the server side of nuxt app lifecycle?
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
.
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
.
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
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 perfectpersistedState
. it may be support to useuseState
.
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
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.
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.
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 👍
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.
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()
});
if you dont wang to use it by yourself, i can write some thing to make it automatic
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");
}
});
yep!(clap hands
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;
}
});
....
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.
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
}
},
@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 }
}
We're encountering the same issue. We always need to manually call $persist in our middlewares to make sure that cookies are actually written. Some kind of automatism would be great.
@MZ-Dlovely Is there a way to call $persist from inside a store function? I call some functions inside a store which manipulates the internal values, but I can't call $persist from inside the store.
For me I could persist a pinia store server side and client side by calling $persist
like this:
export default defineNuxtRouteMiddleware((to) => {
// get the city and country from the route
const city = getRouteParam(to.params.city);
const country = getRouteParam(to.params.country);
// save these value to the app store
const appStore = useAppStore();
appStore.citySlug = city;
appStore.countrySlug = country;
// needed to persist state server side
appStore.$persist();
});
I know the docs mention that you shouldn't need to do this.
However I think in this circumstance it's ok.