question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Nuxt universal + prod - SSR/service plugins broken after page refresh

See original GitHub issue

Steps to reproduce

Setup a very basic nuxt + feathers-vuex example as per the docs. Nuxt mode should be universal. No auth code necessary, but add at least one service using the makeServicePlugin, and have it wired to a working feathers API with some dummy data.

Setup a basic page to test SSR - here, we’re going to call a feathers-vuex service method in fetch():

// pages/index.vue
<template>
 <div>
   <pre>{{ $store.state.conversations }}</pre>
 </div>
</template>
<script>
import { models } from 'feathers-vuex';
export default {
  data: () => ({}),
  async fetch({ store, params }) {
    console.log('start fetch() | store.state.conversations = ', store.state.conversations);
    const { conversation } = models.api;
    await conversation.get(1);
    console.log('end fetch() | store.state.conversations = ', store.state.conversations);
  }
};
</script>
  1. Start a dev server (e.g. yarn dev) - visit the page & refresh.

fw-ok

^ All good, store is clean and then populated on each request. SSR works.

  1. Now start a production server, e.g. yarn build && yarn start
  2. Visit the test page - trying refreshing

fw-bug

Expected behavior

SSR works on every request.

Actual behavior

SSR breaks after first request (store state is only being correctly populated on the first request).

Debugging

If you add a mutation logger plugin to vuex, you’ll see that in dev mode we get the correct cycle of state being cleared and populated on each page request:


**First request**

begin fetch() | store.state.conversations =  {
  ids: [],
  keyedById: {},
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}
conversations/setPending get
conversations/addItem Conversation {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/addItem Conversation {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/unsetPending get
end fetch() | store.state.conversations {
  ids: [ 7 ],
  keyedById: {
    '7': Conversation {
      id: 7,
      title: null,
      creatorId: 5,
      recipientId: 1,
      postId: 10011,
      createdAt: '2020-04-23T22:54:26.062Z',
      updatedAt: '2020-04-23T22:54:26.062Z'
    }
  },
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}

***Page Reload***

begin fetch() | store.state.conversations =  {
  ids: [],
  keyedById: {},
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}
conversations/setPending get
conversations/addItem Conversation {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/addItem Conversation {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/unsetPending get
end fetch() | store.state.conversations {
  ids: [ 7 ],
  keyedById: {
    '7': Conversation {
      id: 7,
      title: null,
      creatorId: 5,
      recipientId: 1,
      postId: 10011,
      createdAt: '2020-04-23T22:54:26.062Z',
      updatedAt: '2020-04-23T22:54:26.062Z'
    }
  },
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}

For prod – only the first page request cycle is healthy:


**First request**

begin fetch() | store.state.conversations =  {
  ids: [],
  keyedById: {},
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}
conversations/setPending get
conversations/addItem o {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/addItem o {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/unsetPending get
end fetch() | store.state.conversations {
  ids: [ 7 ],
  keyedById: {
    '7': o {
      id: 7,
      title: null,
      creatorId: 5,
      recipientId: 1,
      postId: 10011,
      createdAt: '2020-04-23T22:54:26.062Z',
      updatedAt: '2020-04-23T22:54:26.062Z'
    }
  },
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}

**Page Reload**

begin fetch() | store.state.conversations =  {
  ids: [],
  keyedById: {},
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}
conversations/setPending get
conversations/mergeInstance {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/updateItem o {
  id: 7,
  title: null,
  creatorId: 5,
  recipientId: 1,
  postId: 10011,
  createdAt: '2020-04-23T22:54:26.062Z',
  updatedAt: '2020-04-23T22:54:26.062Z'
}
conversations/unsetPending get
end fetch() | store.state.conversations {
  ids: [],
  keyedById: {},
  copiesById: {},
  tempsById: {},
  tempsByNewId: {},
  pagination: { defaultLimit: null, defaultSkip: null },
  isFindPending: false,
  isGetPending: false,
  isCreatePending: false,
  isUpdatePending: false,
  isPatchPending: false,
  isRemovePending: false,
  errorOnFind: null,
  errorOnGet: null,
  errorOnCreate: null,
  errorOnUpdate: null,
  errorOnPatch: null,
  errorOnRemove: null,
  modelName: 'conversation',
  namespace: 'conversations',
  servicePath: 'conversations',
  autoRemove: false,
  addOnUpsert: false,
  enableEvents: false,
  idField: 'id',
  tempIdField: '__id',
  debug: false,
  keepCopiesInStore: false,
  nameStyle: 'short',
  paramsForServer: [],
  preferUpdate: false,
  replaceItems: false,
  serverAlias: 'api',
  skipRequestIfExists: false,
  whitelist: []
}

You’ll notice in prod after the page refresh the store state is messed up. We get empty state both before and after fetch() completes and the service module attempts to update records instead of adding them.

I haven’t dug much deeper than this, but I noted the globalModels object is not clean on the second request (in prod - it’s fine in dev…?!). It has a leftover reference to the conversations service:

Overwriting Model: models[api][conversation] (global-models.js)

Annnnd, within service-module-actions.js, it’s using this dirty state. Within the page component, we’re getting something different.

Any help would be appreciated! And thanks for the awesome plugin, it’s been a big time saver.

System configuration

Tell us about the applicable parts of your setup.

Module versions (especially the part that’s not working):

NodeJS version:

12.16.1

  "dependencies": {
    "@feathersjs/rest-client": "^4.3.11",
    "@nuxtjs/axios": "^5.6.0",
    "@nuxtjs/dotenv": "^1.4.1",
    "@vue/composition-api": "^0.5.0",
    "consola": "^2.10.1",
    "cross-env": "^5.2.0",
    "express": "^4.16.4",
    "feathers-client": "^2.4.0",
    "feathers-hooks-common": "^4.20.7",
    "nuxt-client-init-module": "^0.1.8",
    "feathers-vuex": "^3.9.1",
    "lodash": "^4.17.15",
    "nuxt": "^2.11.0",
  },

Operating System: OS X 10.14.6 Browser Version: Firefox/Chrome

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:16 (8 by maintainers)

github_iconTop GitHub Comments

2reactions
nakedguncommented, May 2, 2020

@marshallswain Pretty much. For SSR, that code gets executed on each request. SPA - typically only once, unless someone is re-initializing (I don’t see the use case for this on the client though?). Irrespective of the mode, it’s just important the Model state and vuex state are in sync. Right now, store.registerModule (in step 1^) is omitting the preserveState param, so it’s defaulting to clearing state - whereas in 2b^, state is preserved if it exists on the Model. The state difference is the source of the bug.

In theory, preserving state could be useful in some SSR scenarios, so this could be an option for the service. But unless that’s requested, a safer default option IMO is to clean state, since it prevents memory leaks for SSR.

And yes - it fixes my app in SPA/SSR (I use both for mobile/web builds). I’ve also tested memory leaks when spamming SSR routes and it seems fine!

0reactions
nakedguncommented, Jun 16, 2020

@phatj One last thing I might add - I don’t know your setup or constraints, but another approach here is to use a serverless function for SSR instead. Since they are - by design - short lived processes, you don’t have to worry about state pollution (or other annoying SSR gotchas) between requests. Once the client has loaded the SSR bundle, it can then talk to your API server, and pretend like none of this SSR nonsense ever existed.

The other main benefit of serverless here is out of box scaling. Vercel supports SSR for Nuxt (though you can roll your own using AWS lambda + serverless framework). I am leaning towards this route instead, since it generally feels safer than trying to make every 3rd party library I want to use fully SSR compliant.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Nuxt not adding bodyAttr on refresh / ssr - vue.js - Stack Overflow
Unfortunately the body tag is a no no in nuxt. What i have noticed is, that upon refresh the page is rendered with...
Read more >
Creating Server-side Rendered Vue.js Apps Using Nuxt.js
This is a big indication that something is wrong with the application logic. Thankfully, an error will be generated in your browser's console...
Read more >
Nuxt Lifecycle
And if you do, it further depends on the type of server-side rendering you have chosen: Dynamic SSR ( nuxt start ). or...
Read more >
Server-side rendering with Vue and Nuxt.js - LogRocket Blog
In plain terms, server-side rendering (SSR) is a technique where we process web pages on a server by pre-fetching and amalgamating the data, ......
Read more >
Getting Started With Nuxt - Smashing Magazine
In this tutorial, we're going to learn how to create server-side rendered applications by using Nuxt.js, how to configure your application ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found