prazdevs/pinia-plugin-persistedstate

Pinia is not creating cookies when using SSR

Closed this issue · 3 comments

Are you using Nuxt?

  • Check this box if you encountered the issue using Nuxt and the included module.

Describe the bug

When I create/set a state during a component's Setup phase (in Nuxt's SSR), it is expected that the state persists on every page refresh. I would expect the cookie with the new state to be hydrated to the client after the SSR completes.

Store:

import { defineStore } from "pinia"

export const useSessionIdStore = defineStore("sessionId", {
  persist: true,

  state: () => ({ sessionId: Math.random().toString(36).substring(2) as string }),
  getters: {
    getSessionId(state) {
      return state.sessionId
    },
  },
  actions: {},
})

nuxt.config:

(...)
  ssr: true,

  modules: ["@element-plus/nuxt", "@pinia/nuxt", "@pinia-plugin-persistedstate/nuxt"],

  piniaPersistedstate: {
    cookieOptions: {
      sameSite: "lax",
    },
    storage: "cookies",
    debug: true,
  },
(...)

package.json:

{
  "name": "Demo",
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 MODE=development nuxt dev --dotenv .env.development --host 0.0.0.0",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "lint-staged": {
    "*.{js,ts,vue}": "eslint --cache"
  },
  "devDependencies": {
    "@element-plus/nuxt": "^1.0.9",
    "@iconify/vue": "^4.1.1",
    "@nuxt/devtools": "^1.0.6",
    "@nuxt/eslint-config": "^0.1.1",
    "@nuxtjs/robots": "^4.1.7",
    "@nuxtjs/seo": "^2.0.0-rc.21",
    "@nuxtjs/sitemap": "^6.1.0",
    "@pinia-plugin-persistedstate/nuxt": "^1.2.0",
    "@types/aos": "^3.0.7",
    "@types/markdown-it": "^13.0.7",
    "@types/markdown-it-link-attributes": "^3.0.5",
    "@types/node": "^18.17.1",
    "autoprefixer": "^10.4.14",
    "element-plus": "^2.7.2",
    "eslint": "^8.56.0",
    "eslint-config-prettier": "^8.9.0",
    "eslint-plugin-prettier": "^5.0.0",
    "eslint-plugin-simple-import-sort": "^10.0.0",
    "eslint-plugin-vue": "^9.21.1",
    "lint-staged": "^13.2.3",
    "markdown-it-named-headers": "^0.0.4",
    "nuxt": "^3.13.2",
    "nuxt-link-checker": "^3.1.1",
    "nuxt-schema-org": "^3.4.0",
    "nuxt-site-config": "^2.2.18",
    "nuxt-site-config-kit": "^2.2.18",
    "prettier": "^3.0.0",
    "pug": "^3.0.2",
    "sass": "^1.64.2",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.2.2",
    "vue-gtag-next": "^1.14.0",
    "vue-router": "^4.2.5"
  },
  "dependencies": {
    "@chenfengyuan/vue-countdown": "^2.1.2",
    "@pinia/nuxt": "^0.5.1",
    "@sentry/vue": "^8.19.0",
    "aos": "^3.0.0-beta.6",
    "apexcharts": "^3.45.1",
    "gauge-chart": "^1.0.0",
    "highlight.js": "^11.9.0",
    "markdown-it": "^14.0.0",
    "markdown-it-link-attributes": "^4.0.1",
    "pinia": "^2.2.2",
    "vue": "^3.2.47",
    "vue-country-flag-next": "^2.3.2",
    "vue-tsc": "^2.1.6",
    "vue3-apexcharts": "^1.4.4",
    "vue3-lottie": "^3.2.0"
  }
}

Then anywhere in the component's setup:

console.log("sessionId: " + useSessionIdStore().sessionId)

And you'll see that on every refresh this session Id is different, and no cookie gets created to persist it ever.

Wasn't this supposed to work in a SSR friendly way?

Reproduction

reproduction above

System Info

System:
    OS: Linux 6.8 Linux Mint 21.3 (Virginia)
    CPU: (16) x64 AMD Ryzen 7 5800H with Radeon Graphics
    Memory: 11.72 GB / 27.26 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 22.11.0 - ~/.nvm/versions/node/v22.11.0/bin/node
    npm: 10.9.0 - ~/.nvm/versions/node/v22.11.0/bin/npm
  Browsers:
    Brave Browser: 129.1.70.126
    Chromium: 129.0.6668.89

Used Package Manager

npm

Validations

Hi, while the reproduction is not what is expected (please read the article on what proper reproduction is), I think the issue is that persistence only happens on mutation (calling store.sessionId = 'something' somewhere after store is initialized), and not when stores are created. You can still use $persist if you need to force persistence at a given moment without mutating the state.

Sorry for the reproduction. But you nailed the issue directly.

I find it very strange that persist is set to true, but the very initial state (set when the store is created) does not get persisted, when it is a valid state like any other. Not a bug, but a design decision, I get it. But it seems incoherent, because the initial state should be no different than any other state IMO. I.e. setting the initial state should be seen as a mutation I guess.

And this hack was the best solution I found that:

  1. Keeps sessionId being string instead of string|null
  2. Persists the initial state on store creation
  3. Refrains me from polluting other parts of the project with a forced initial mutation

I added useCookie, and removed the persist: true, so I manually took over the store's persistence.

import { useCookie } from "#app" // Use Nuxt's useCookie helper
import { defineStore } from "pinia"

export const useSessionIdStore = defineStore("sessionId", {
  state: () => {
    const sessionIdCookie = useCookie("sessionId")

    // Set sessionId from cookie if it exists; otherwise, generate a new one and store it in the cookie
    const sessionId = sessionIdCookie.value || Math.random().toString(36).substring(2)
    sessionIdCookie.value = sessionId

    console.log("SessionId: ", sessionId)

    return {
      sessionId, // Initialized directly, never null
    }
  },
  getters: {
    getSessionId(state): string {
      return state.sessionId
    },
  },
})

IMO this is the behavior I would expect by default when persist: true.
Setting an initial state is different from not setting it at all, so rendering the same persisting behavior (none) for these two different scenarios makes no sense to me from a logical perspective. Any state change would be persisted, including the initial one.