Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deal with async data retrieval with Vuex / Vue

I have a simple app with a common stack :

  • A backend server (Rails)
  • A frontend app (Vue)
  • A database (PG)

The Vue app fetch data from the backend using an action of the Vuex store library as so :

// store/store.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as MutationTypes from '@/store/mutation-types';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    investment: {},
  },
  mutations: {
    [MutationTypes.SET_INVESTMENT_SHOW](state, investment) {
      state.investment = investment;
    },
  },
  actions: {
    fetchInvestment({ commit }, id) {
      InvestmentsApi.get(id).then((response) => {
        commit(MutationTypes.SET_INVESTMENT_SHOW, response.data);
      });
    },
  },
  getters: {
    participation: state =>
      state.investment.included[0],
  },
});

The action is called in the created lifecycle hook of my component as so :

// components/Investment.vue

import { mapActions, mapGetters } from 'vuex';
export default {
  name: 'Investment',
  computed: {
    ...mapState(['investment']),
    ...mapGetters(['participation']),
  },
  created() {
    this.fetchData(this.$route.params.id);
  },
  methods: mapActions({
    fetchData: 'fetchInvestment',
  }),
};

There is a problem in the code I've written above, I actually use the computed value 'participation' in my template like this :

<BaseTitleGroup
  :subtitle="participation.attributes.name"
  title="Investissements"
/>

And of course, because I use participation at the time the component renders itself, I get this error from the getter method :

Error in render: "TypeError: Cannot read property '0' of undefined"

found in

---> <InvestmentSummary> at src/views/InvestmentSummary.vue
       <App> at src/App.vue
         <Root>

I think there are several solutions to solve this problem and I'm wondering which one is best practice or if there is a better one.

  1. The first solution would be to put a v-if attribute in my template to prevent the element from rendering while waiting for the data
    • Con : The rendering offset (the element starts rendering when the data is there) ?
    • Con : I would have to do it for every single component of my app that deals with async data, intuitively I would prefer to deal with this somewhere else (maybe the store?).
  2. Render the element and put fake data in the store, like "Loading..."
    • Con : The little glitch the user sees when loading his page is ugly, when the text switches from loading to the real text.
    • Con : The empty version of my store would be painful to write and super big when my app scales
  3. Change the getter to return the initial empty data, and not the store
    • Con : Getters get more complicated
    • Con : What about data that do not need a getter (maybe they are directly accessible from the state)
  4. Something else ?

I'm looking for the best solution to deal with this pattern, even if it's one of the above, I'm just not sure which one would be the best. Thanks a lot for reading ! Also, I use vue framework but I think it is more of a general question about modern javascript framework handling of async data and rendering.

Sorry for the long post, here is a potato ! (Oops, not on 9gag ;) )

like image 339
Uelb Avatar asked Sep 21 '18 07:09

Uelb


2 Answers

In Angular, there is the Elvis (safe navigation) operator, which is a concise way to handle reactive data that eventually arrives.

If it was available in the Vue template compiler, your template would look like this:

<BaseTitleGroup
  :subtitle="participation?.attributes?.name"
  title="Investissements"
/>

However, Evan You says that it sounds like a code smell.

Your model/state should be as predictable as possible.

Trying to expand that comment into your context, I think it means that your template knows more about your data structure than your store.

template

"participation.attributes.name"

which equates to:

state.investment.included[0].attributes.name

store

state: {
  investment: {},
},

Since the getter is there to serve the Component (and it's template), I would opt for enhancing the getter.

getters: {
  participation_name: state => {
    return 
      (state.investment.included 
       && state.investment.included.length
       && state.investment[0]
       && state.investment[0].attributes
       && state.investment[0].attributes.name)
      || null;
},

<BaseTitleGroup
  :subtitle="participation_name"
  title="Investissements"
/>

But if you wanted elvis functionality, you could provide it in a mixin.

var myMixin = {
  computed: {
    elvis: {
      get: function() {
        return (known, unknown) => {
          // use regex split to handle both properties and array indexing
          const paths = unknown.split(/[\.(\[.+\])]+/); 
          let ref = known
          paths.forEach(path => { if (ref) ref = ref[path] });
          return ref;
        }
      }
    },
  }
}

export default {
  name: 'Investment',
  ...
  mixins: [myMixin],
  computed: {
    ...mapState(['investment']),
    participation_name() {
      return this.elvis(this.investment, 'included[0].attributes.name')
    }
  },
  ...
};
like image 133
Richard Matsen Avatar answered Oct 16 '22 23:10

Richard Matsen


I don't think there is the best solution, just choose one and use it everywhere instead of mixing them all.

v-if however might be better in case you want to render data from a nested property - v-if="object.some.nested.property v-for="el in object.some.nested.property" will work but pre-defining object = {} will not (it will throw an error that some is undefined and you are trying to access it).

I wouldn't put any fake data like in your example, but you can use ES6 Classes to define default objects and set them as your default values. This also fixes the above pre-defining problem as long as your class object has a proper structure (and it's also syntatically transparent and easy to understand).

As for the third option - giving empty data to getters doesn't have to be complicated - just change your getter to:

getters: {
    participation: state =>
      state.investment.included[0] || new DefaultParticipationObject() // I don't know what's in included array
  },

This uses state.investment.included[0] if it's defined and a default object otherwise.

like image 2
dziraf Avatar answered Oct 16 '22 23:10

dziraf