pinia-plugin-persistedstate icon indicating copy to clipboard operation
pinia-plugin-persistedstate copied to clipboard

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

Open d0peCode opened this issue 1 year ago • 28 comments

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

d0peCode avatar Jul 16 '23 22:07 d0peCode

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

MZ-Dlovely avatar Jul 17 '23 02:07 MZ-Dlovely

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.

d0peCode avatar Jul 17 '23 02:07 d0peCode

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 avatar Jul 17 '23 02:07 MZ-Dlovely

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

MZ-Dlovely avatar Jul 17 '23 03:07 MZ-Dlovely

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.

d0peCode avatar Jul 17 '23 03:07 d0peCode

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

MZ-Dlovely avatar Jul 17 '23 03:07 MZ-Dlovely

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 avatar Jul 17 '23 03:07 d0peCode

Also cookie is not created: image

d0peCode avatar Jul 17 '23 05:07 d0peCode

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.

MZ-Dlovely avatar Jul 17 '23 07:07 MZ-Dlovely

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

d0peCode avatar Jul 17 '23 07:07 d0peCode

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 avatar Jul 17 '23 07:07 MZ-Dlovely

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.

MZ-Dlovely avatar Jul 17 '23 07:07 MZ-Dlovely

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

Ena-Heleneto avatar Jul 26 '23 09:07 Ena-Heleneto

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

d0peCode avatar Jul 26 '23 10:07 d0peCode

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.

MZ-Dlovely avatar Jul 26 '23 11:07 MZ-Dlovely

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.

d0peCode avatar Jul 26 '23 13:07 d0peCode

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 👍

prazdevs avatar Jul 26 '23 15:07 prazdevs

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 avatar Jul 26 '23 16:07 MZ-Dlovely

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 avatar Jul 27 '23 04:07 MZ-Dlovely

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

MZ-Dlovely avatar Jul 27 '23 04:07 MZ-Dlovely

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");
  }

  
});

erdemyunsel avatar Nov 22 '23 12:11 erdemyunsel

yep!(clap hands

MZ-Dlovely avatar Nov 22 '23 13:11 MZ-Dlovely

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;
    }
  });

....

erdemyunsel avatar Nov 22 '23 13:11 erdemyunsel

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.

RomainMazB avatar Dec 21 '23 10:12 RomainMazB

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 avatar Jun 26 '24 10:06 localusercamp

@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 }
}

RomainMazB avatar Jul 04 '24 07:07 RomainMazB

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.

PatrickBauer avatar Jul 18 '24 08:07 PatrickBauer

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.

ianjamieson avatar Jul 19 '24 11:07 ianjamieson