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 SSR + AsyncData retrieved from FeathersVuex on page reload is not reactive

See original GitHub issue

Steps 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:open
  • Created 3 years ago
  • Reactions:1
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
njbarrettcommented, Oct 12, 2020

Hi all, I have expanded upon @tylermmorton 's workaround function a little with the following:

  1. Handles already hydrated objects
  2. Errors if an invalid modelName is passed. (Assumes always passing a string modelName)
export const hydrateObject = (obj, modelName) => {
  // Already hydrated.
  if (obj && obj.constructor && obj.constructor.name === modelName) {
    return obj;
  }

  if (typeof modelName !== 'string' || !models.api[modelName]) {
    throw new Error('Please pass a valid model name to hydrateObject');
  }

  const Model = models.api[modelName];

  // Return the feathers-vuex record from the obj's id.
  if (typeof obj.id !== 'undefined' && typeof Model !== 'undefined') {
    return Model.getFromStore(obj.id);
  }

  return obj;
};
0reactions
tylermmortoncommented, May 14, 2020

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 a fetch() 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:

import { hydrateObject } from "~/plugins/feathers";
// equates to
export const hydrateObject = function hydrateObject(obj, model) {
  if (typeof model == 'string') {
    model = models.api[model]
  }
  // return the feathers-vuex record from the obj's id.
  if (obj.id != undefined && model != undefined) {
    return model.getFromStore(obj.id)
  }
}

additional props:

    item: {
      type: Object,
      required: true
    },
    model: {
      type: String || Object,
      required: false
    },

additional data:

  data: () => ({
    clone: null,
    isEditing: false,
    isHydrated: false
  }),

additional computed props:

    form() {
      if (this.isHydrated) {
        return this.clone;
      } else {
        return this.item;
      }
    },

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.

    edit() {
      if (process.client) {
        let modelInstance = hydrateObject(this.item, this.model);
        if (modelInstance != undefined) {
          this.clone = modelInstance.clone();
          this.isHydrated = true;
          this.isEditing = true;
        }
      }
    },

and we export everything

  render() {
    const { form, edit, save, reset, remove, isDirty, isNew, isEditing } = this;
    return this.$scopedSlots.default({
      form,
      edit,
      save,
      reset,
      remove,
      isDirty,
      isNew,
      isEditing
    });
  }

and usage would look like this:

    <SSRFormWrapper :item="post" model="Post">
      <template v-slot="{ form, save, reset, edit, isEditing }">
        <div>
          <v-editor v-model="form.content" :editable="isEditing"/>

          <div v-if="isEditing">
            <v-btn @click="save">Save</v-btn>
            <v-btn @click="reset">Cancel</v-btn>
          </div>
          <div v-else>
            <v-btn @click="edit">Edit</v-btn>
          </div>
        </div>
      </template>
    </SSRFormWrapper>
Read more comments on GitHub >

github_iconTop Results From Across the Web

Nuxt SSR + AsyncData retrieved from FeathersVuex on page reload ...
Nuxt SSR + AsyncData retrieved from FeathersVuex on page reload is not reactive.
Read more >
nuxtjs : how to force page reload and call asyncData()?
One way, if I understand your issue correctly, is to use the Vuex store for storing whatever you are fetching in fetch /...
Read more >
vuejs/vue - Gitter
I have a console.log the object in an input handler and can see that the properties are not "reactive getter" (as compared to...
Read more >
A curated list of awesome things related to Vue.js - Gitee
Responsive ; Mobile; Component Collections; Admin Template; Server-side rendering ... Nuxt Type An example Vue project with Nuxt for routing/SSR to demo page...
Read more >
Nuxt | FeathersVuex
Access $FeathersVuex models in Nuxt asyncData ... whitelist: ['$regex', '$options'], enableEvents: process.client // No events for SSR server } ) ...
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