Nuxt SSR + AsyncData retrieved from FeathersVuex on page reload is not reactive
See original GitHub issueSteps to reproduce
- Set up a Nuxt + FeathersVuex project per the documentation.
- Create a page that queries a FeathersVuex service in asyncData
- Go directly to said page using client side routing
- Notice that the FeathersVuex query is run on the Nuxt server beforing serving the page
- Notice that the component data merged from asyncData is not a model instance, but instead just a normal Object.
/pages/post/_id.vue
async asyncData({ app, route }) {
const { Post } = models.api;
const post = await Post.get(route.params.id);
return { post };
},
Expected behavior
When Nuxt serves a page to the client on first load/page reload, objects received from FeathersVuex in asyncData should be model instances. Note that any subsequent loads through the Vue/Nuxt router in Universal or SPA mode are working as intended.
Actual behavior
The data loaded during asyncData is merged with the component’s data field but loses its ties to the FeathersVuex store. Without those ties, functions like clone() or save() cannot be called. This also breaks components like FeathersVuexFormWrapper.
My findings & workaround
I’ve made a temporary solution to get me moving forward again with my project. On the client side only, whenever the component is created, I will individually hydrate each object using a function I exported from my feathers plugin.
The hydrateObject
function will just return the corresponding record in the Vuex store based on the object’s id and api passed in. I assume the object is already in the store (thanks to calls to hydrateApi on nuxtClientInit), so no redundant calls to the server will happen here. This seems to work OK at replacing the normal Object with a model instance but can be cumbersome when there’s a lot of objects to hydrate:
/plugins/feathers.js
async function hydrateObject(obj, api) {
if (obj.id != undefined && api != undefined) {
const record = await api.get(obj.id);
return record;
}
}
...
export { hydrateObject, ... }
/pages/post/_id.vue
import { models, hydrateObject } from "~/plugins/feathers";
async created() {
if (process.client) {
const { Post } = models.api;
this.post = await hydrateObject(this.post, Post);
// this.post is now a model instance so it has functions like clone()
this.form = this.post.clone();
}
},
Theres a couple of caveats I’ve run into with this solution, the main one being that if you are following the form binding “clone/commit” pattern or using the FeathersVuexFormWrapper, your data is not turned into a model instance until created()
is called, causing any components that rely on the model instance functions to fail.
Right now the only solution I can think of to this is to roll my own version of FeathersVuexFormWrapper that can take either a normal object or a model instance on page load, and hydrates the object if needed when created() on the client is called.
System configuration
I’ve set up my project as close to the FeathersVuex + Nuxt documentation as I can. When setting up the store, I’ve made sure to include the call to hydrateApi() provided by FeathersVuex, which seems to be hydrating the store data properly.
export const actions = {
nuxtServerInit({ commit, dispatch, state }, { req }) {
return initAuth({
commit,
dispatch,
req,
moduleName: "auth",
cookieName: "feathers-jwt"
}).then(async () => {
if (state.auth.accessToken) {
// auth the current user from the JWT token obtained above
await dispatch("auth/authenticate", {
accessToken: state.auth.accessToken,
strategy: "jwt"
});
}
});
},
nuxtClientInit({ state, dispatch, commit }, context) {
if (models) {
hydrateApi({ api: models.api });
}
if (state.auth.accessToken) {
return dispatch("auth/onInitAuth", state.auth.payload);
}
}
};
Module versions
"dependencies": {
"@feathersjs/authentication": "^4.5.3",
"@feathersjs/authentication-client": "^4.5.4",
"@feathersjs/authentication-local": "^4.5.4",
"@feathersjs/authentication-oauth": "^4.5.4",
"@feathersjs/configuration": "^4.5.3",
"@feathersjs/errors": "^4.5.3",
"@feathersjs/express": "^4.5.4",
"@feathersjs/feathers": "^4.5.3",
"@feathersjs/socketio": "^4.5.4",
"@feathersjs/socketio-client": "^4.5.4",
"@feathersjs/transport-commons": "^4.5.3",
"@vue/composition-api": "^0.5.0",
"compression": "^1.7.4",
"cookie-storage": "^6.0.0",
"cors": "^2.8.5",
"feathers-hooks-common": "^5.0.3",
"feathers-sequelize": "^6.1.0",
"feathers-vuex": "^3.9.2",
"helmet": "^3.22.0",
"nuxt": "^2.12.2",
"nuxt-client-init-module": "^0.1.8",
"pg": "^8.0.3",
"sequelize": "^5.21.7",
"serve-favicon": "^2.5.0",
"socket.io-client": "^2.3.0",
"tiptap": "^1.27.1",
"tiptap-extensions": "^1.29.1",
"winston": "^3.2.1"
},
NodeJS version: v10.7.0
Operating System: MacOS Mojave Version 10.14.5 (18F132)
Browser Version: Chrome Version 81.0.4044.129 (Official Build) (64-bit)
– This may just be an issue with Nuxt, but I’m interested to see what others think of this.
Thanks in advance Tyler
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:5 (4 by maintainers)
Top GitHub Comments
Hi all, I have expanded upon @tylermmorton 's workaround function a little with the following:
I haven’t forgotten about this issue. I’m still playing around in my personal project and trying different solutions. If anyone has any ideas on this I’d be happy to try them out.
I haven’t figured out yet how to automatically hydrate the objects when Nuxt serves the page on a reload. There just isn’t a good hook to use for this. If Nuxt had a
fetched()
hook that gets called after afetch()
query completes, that would be a great place for this. I’ve tried adding a watcher on the Nuxt$fetchState
but that wasn’t working for me.I’ve instead adapted the clone/commit strategy with an “edit” toggle for the form or editor you’re trying to use this clone/commit pattern with. I’ve taken the FeathersVuexFormWrapper and modified it. I apologize in advance if some of this is unclear. Its rough ideas for now
The utility function I wrote earlier:
additional props:
additional data:
additional computed props:
and finally a new method to be exported. This is the most important because it turns the normal JS object into a model reference whenever the “edit” button is clicked.
and we export everything
and usage would look like this: