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

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

About this issue

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

Commits related to this issue

Most upvoted comments

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