Nuxt.js: Server state restore is executed after Vuex plugins

Created on 26 Jun 2017  Â·  30Comments  Â·  Source: nuxt/nuxt.js

In index.js, createState() is called first (and plugins are initialized), then the server state is restored.

This effectively disables plugins using Local Storage to persist state (client side data is overriden with SSR data every time). A brief code example is attached below for easier understanding.

I propose to inject a SSR restore plugin at the first of Vuex plugins constructor parameter. This way, SSR restore happens before other plugins.

Code:

import createPersistedState from 'vuex-persistedstate'

export const state = () => ({
  // ... defaults ...
})

export const plugins = [
  createPersistedState()
]

This question is available on Nuxt.js community (#c841)
available soon question

Most helpful comment

@PrimozRome There are two ways to fix this window is undefined error for non spa mode:

  1. Recommended way: use ssr: false while registering the plugin in nuxt.config.js as below:
plugins: [
  { src: '~plugins/persistedstate.js', ssr: false }
]
  1. Alternate way: use isClient or isServer from the context to conditionally run window.onNuxtReady() in your plugin like below:
import createPersistedState from 'vuex-persistedstate'

export default ({store, isHMR, isServer, isClient}) => {
  // In case of HMR, mutation occurs before nuxReady, so previously saved state
  // gets replaced with original state received from server. So, we've to skip HMR.
  // Also nuxtReady event fires for HMR as well, which results multiple registration of
  // vuex-persistedstate plugin
  if (isHMR) return

  if (isClient) {
    window.onNuxtReady((nuxt) => {
      createPersistedState()(store) // vuex plugins can be connected to store, even after creation
    })
  }
}

All 30 comments

This is an interesting conundrum. Keep in mind that the following code breaks SSR, because the server doesn't know about the local persisted state, but you can prepend this function to the plugins list, like so:

function restoreState(store) {
  if (process.browser) {
    if (store) {
      // Replace store state before calling plugins
      if (window.__NUXT__ && window.__NUXT__.state) {
        store.replaceState(window.__NUXT__.state)
        delete window.__NUXT__.state;
      }
    }
  }
}

export const plugins = [restoreState, createPersistedState()];

A quick explanation: the internal version of the Nuxt state restore does a check to see if __NUXT__.state exists, so we use it up, delete it, and then rehydrate the state from localStorage. When the internal version carefully checks if __NUXT__.state exists, it finds that it does not, and doesn't overwrite state.

That's a cool solution.

I think documenting this is enough (making this change global is hard and breaks things). @alexchopin how do you think?

Uh oh, although it's something by design, this will generate a warning:

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

It would be better if we could safely rerender the states (triggering reactive bindings, instead of discarding SSR DOM).

Agreed, how do you do this?

I am also looking for a solution, I am trying to build a shopping cart and need persisted vuex data.
as @ishitatsuyuki says @dts example gives warning

Yeah - we need to execute something on client-side only - so that's probably the fetch() command

Any progress on this? I have the same problem :/

With next release (rc6) store state rehydration will be done after plugins. So we have a chance to change it. Also official vue-persistedstate for nuxt:

https://github.com/nuxt/nuxt.js/tree/dev/examples/vuex-persistedstate

Well, I think you meant before. Anyway, this is basically bringing the same functionality as @dts showed, and the client-only rehydration problem will never resolve due to the architecture.

the client-only rehydration problem will never resolve due to the architecture.

@ishitatsuyuki Unfortunately yes as SSR rendering anyway needs the same state of client side for a matching DOM tree and we may need some advanced workarounds like store all state in cookie for server-side working too. This is why spa mode was used in the mentioned example. Any workaround for this from the community would be appreciated.

After playing around a bit, I've come up with a solution to use vuex-persistedstate plugin without setting the spa mode.

Solution is simple. Just create a nuxt plugin with following code that utilises onNuxtReady handler and register that plugin with ssr: false in nuxt.config.js

import createPersistedState from 'vuex-persistedstate'

export default ({store, isHMR}) => {
  // In case of HMR, mutation occurs before nuxReady, so previously saved state
  // gets replaced with original state received from server. So, we've to skip HMR.
  // Also nuxtReady event fires for HMR as well, which results multiple registration of
  // vuex-persistedstate plugin
  if (isHMR) return

  window.onNuxtReady((nuxt) => {
    createPersistedState()(store) // vuex plugins can be connected to store, even after creation
  })
}

Sharing with hope, that someone will find this trick helpful as well.

@mnishihan how did you get this working in ssr mode? I get window is not defined at line window.onNuxtReady((nuxt) => {

plugins/vuex-persistance.js

import createPersistedState from 'vuex-persistedstate'

export default ({store, isHMR}) => {
  // In case of HMR, mutation occurs before nuxReady, so previously saved state
  // gets replaced with original state received from server. So, we've to skip HMR.
  // Also nuxtReady event fires for HMR as well, which results multiple registration of
  // vuex-persistedstate plugin
  if (isHMR) return

  window.onNuxtReady((nuxt) => {
    createPersistedState()(store) // vuex plugins can be connected to store, even after creation
  })
}

@PrimozRome There are two ways to fix this window is undefined error for non spa mode:

  1. Recommended way: use ssr: false while registering the plugin in nuxt.config.js as below:
plugins: [
  { src: '~plugins/persistedstate.js', ssr: false }
]
  1. Alternate way: use isClient or isServer from the context to conditionally run window.onNuxtReady() in your plugin like below:
import createPersistedState from 'vuex-persistedstate'

export default ({store, isHMR, isServer, isClient}) => {
  // In case of HMR, mutation occurs before nuxReady, so previously saved state
  // gets replaced with original state received from server. So, we've to skip HMR.
  // Also nuxtReady event fires for HMR as well, which results multiple registration of
  // vuex-persistedstate plugin
  if (isHMR) return

  if (isClient) {
    window.onNuxtReady((nuxt) => {
      createPersistedState()(store) // vuex plugins can be connected to store, even after creation
    })
  }
}

@mnishihan yes I get this... I was able to figure out the ssr: false flag.

But I want to read the local storage when I come into the app from refresh in browser... When I do refresh app is served from server side and I can't read the local storage there because ssr: false coms into effect right?

My vuex store is persisted in the local storage:

screen shot 2017-09-28 at 00 40 15

but when I refresh browser the vuex store is not read from local storage...

screen shot 2017-09-28 at 00 41 30

I just don't seem to get it work...

@primozrome In my app this is working fine. You might want to try several things to get it working:

  1. Upgrade nuxt package (I'm using latest rc11)
  2. Register the plugin after all plugins that might mutate your vuex store and before any plugin that depends on the persisted state of vuex store.
  3. Restart your dev server and try again after deleting vuex entry from your local storage.

@mnishihan Nothing works. Looks like middleware is not even executed in the client on browser refresh... Tried all three.

So if you press refresh button in the browser, page/layout middleware is executed in isClient = true context?

I'm being totally confused. Why are your calling this as middleware? This
should be registered as a plugin, not a route middleware. Can you share
your nuxt.config.js file?
On Thu, Sep 28, 2017 at 6:54 PM, Primož Rome notifications@github.com
wrote:

Nothing works. Looks like middleware is not even executed in the client on
browser refresh...

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/nuxt/nuxt.js/issues/972#issuecomment-332827167, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ABHnKDbs__m5nOEgWXPx2yN1joKJ2W7wks5sm5cDgaJpZM4OE75r
.

@mnishihan I have the vuex-persistance setup in the plugins folder as you explained above. Nothing wrong here this works as expected.

But then I have auth.js middleware where I check the local storage for auth-token. If auth-token exist and is valid I would like to automatically log in user on refresh, otherwise redirect him to the login page. For this I commits some store mutations which needs to be saved into local storage.

But when I press refresh the middleware is never executed in isClient=true context. As you see on my photos above store is never restored from local storage. So for every refresh app takes me back to the login page which is not something of a good UIX experience.

Now I got it. Server side middleware doesn't know anything about client
side persisted state and client side middleware doesn't execute in page
refresh or first load.

To overcome this, write your middleware code in onNuxReady event handler
(if you want to set the logic globally) or in fetch() method of layout/page
conditionally when isClient is true or even better in mounted() method (as
it only executes at client side).

To understand what I mean, read the discussion above and also see the
official example of auth routes in nuxt guide.
On Thu, Sep 28, 2017 at 7:07 PM, Primož Rome notifications@github.com
wrote:

@mnishihan https://github.com/mnishihan I am not I have this setup in
the plugins as you explained above. nothing wrong here this works as
expected.

But then I have auth.js middleware where I check the local storage for
auth-token. If auth-token exist and is valid I would like to automatically
log in user on refresh, otherwise redirect him to the login page. Also this
auth middleware commits some store mutations.

But when I press refresh the middleware is never executed in isClient=true
context so I would be able to read auth token and automatically log in
user. So for every refresh app takes me back to the login page which is not
something of a good UIX experience.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/nuxt/nuxt.js/issues/972#issuecomment-332830464, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ABHnKFXswZstmpo7ezpiIZX6Qy7AYt-Dks5sm5n_gaJpZM4OE75r
.

@mnishihan uff thanks a lot on this. So the client side middleware is not executed on page refresh. Damn that's what I did not know. Spent days figuring this out 👎 .

To overcome this, write your middleware code in onNuxReady event handler

Where exactly is this defined?

onNuxtReady event handler is the place where you're already calling
createPersistedState()(store) in your plugin. Just add any code, after that
line, that depends on the restored client side persisted state and you want
to execute that code globally once after each page reload. Remember like
all nuxt plugin, this code will execute only once, after page reload. So,
if you want to validate auth & redirect, the place would be to write the
redirection logic in layout's/page's fetch() or monuted() method.

for anyone needing a workaround to persist state across server and client together
plugins/persistedstate.js

import createPersistedState from 'vuex-persistedstate'
import * as Cookies from 'js-cookie'
import cookie from 'cookie'

export default ({store, req, isDev}) => {
  createPersistedState({
      key: 'your_key',
      paths: ['state1', 'state2',...so_on],
      storage: {
        getItem: (key) => process.client ? Cookies.getJSON(key) : cookie.parse(req.headers.cookie||'')[key],
        setItem: (key, value) => Cookies.set(key, value, { expires: 365, secure: !isDev }),
        removeItem: (key) => Cookies.remove(key)
      }
  })(store)
}

nuxt.config.js

plugins: [
  { src: '~plugins/persistedstate.js' }
]

@nikugogoi Thanks so much. I'm not sure if this is the best way but it looks pretty straight forward. It does exactly what I want.

@busheezy keep in mind that you are storing data in the cookie which is tranferred along with server calls from client to server. More the data, more the expense in data transfer. So you should be careful about how much state you persist using paths. So, this solution has kind of a performance caveat one might say.

You might want to check out the new updated doc here https://github.com/robinvdvleuten/vuex-persistedstate#nuxtjs if your use case allows you to persist state only on client side.

Thanks. I'm using the paths option to only store a couple of things. I should be alright. It's only 67 chars after being encoded. I found your comment here after looking through their issues/docs. The server side render is perfect for my use. Thanks, again.

@nikugogoi thanks for your solution which seems to work like a charm

but could you explain why you're setting secure: !isDev?

Because I tried to set secure: true and it doesn't work anymore with npm run dev nor with nuxt build && nuxt start...

so I wanted to know, in which case this would work in production env?

EDIT: I think I've found the answer myself:

Note: Insecure sites (http:) can't set cookies with the "secure" directive anymore (new in Chrome 52+ and Firefox 52+).

Source: https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Set-Cookie

@nikugogoi
It works but not 100%. If you redirect from fetch or asyncData into a sections that have middleware, store get modify by mutators and after is restore to the previous value.

@Oliboy50 secure is to check if you are in https or not.

@furyscript redirect from fetch or asyncData server side?? because in client side redirect() performs a simple window.location.replace(path)
EDIT : even in server side nuxt returns a 302 redirect response

Uhm see this repo: https://github.com/furyscript/nuxt-redirect-error

You need to login, go to inspire page and make sure the first http call is a 200 request. After that try to refresh page (F5 into /inspire) until the http call return a 401 error, so you see that you return to home and not in login page.

As you can see, not work 100% @nikugogoi

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings