Vue structuring with Vuex and component-specific data

I see a lot of Vue.js projects using this structure:

├── main.js
├── api
│   └── index.js
│   └── services           #containing files with api-calls
│       ├── global.js
│       ├── cart.js
│       └── messages.js
├── components
│   ├── Home.vue
│   ├── Cart.vue
│   ├── Messages.vue
│   └── ...
└── store
    ├── store.js
    ├── actions.js  #actions to update vuex stores
    ├── types.js
    └── modules
        ├── global.js
        ├── cart.js
        └── ...

(An example with this structure is 'Jackblog'.)

So, for example, Cart.vue wants to update the inCart data in Vuex. To do that, the Cart imports actions.js:

import { inCart } from '../../store/actions'

The actions.js imports the api's index.js so it can connect to the api. And then it updates the values in the Vuex store.

Ok, so that is clear for me. But now, I want to work on the Messages.vue module. This module should connect to the api as well to get all the messages, but it is not necessary to store the results in Vuex. The only component that needs the data is Messages.vue itself, so it should be stored in the component's data() only.

Question: I can't import actions.js inside Messages.vue because the action shouldn't update Vuex. But I can't move the actions.js to the api directory, because that breaks the logic of putting all files that add data to the store in the store-directory. Besides that, the logic should be placed inside Messages.vue. For example when the api returns an error, a local error-constant should be set. So it cannot be handled by a separate file.

What is the recommended application structure for making api calls and store them in vuex or local data()? Where to place the actions file, the api files, and so on? When looking to the Jackblog example it supports Vuex data only. How to restructure this to support both?


Solution 1:

I am using axios as HTTP client for making API calls, I have created a gateways folder in my src folder and I have files for each backend, creating axios instances, like following

myApi.js

import axios from 'axios'
export default axios.create({
  baseURL: 'http://localhost:3000/api/v1',
  timeout: 5000,
  headers: {
    'X-Auth-Token': 'f2b6637ddf355a476918940289c0be016a4fe99e3b69c83d',
    'Content-Type': 'application/json'
  }
})

These same instances are used in both component and vuex actions to get the data, following are details of both ways.

Populating component data

If the data is being used only in the component, like your case of Messages.vue, you can have a method which will fetch data from the api like following:

export default {
  name: 'myComponent',
  data: () => ({
    contents: '',
    product: []
  }),
  props: ['abc'],
  methods: {
    getProducts (prodId) {
       myApi.get('products?id=' + prodId).then(response => this.product = response.data)
       },
       error => {
          console.log('Inside error, fetching products failed')
          //set error variable here
       })
    }
    ..... 

Populating Vuex data

If you are maintaining product related data in a dedicate vuex module, you can dispatch an action from the method in component, which will internally call the backend API and populate data in the store, code will look something like following:

Code in component:

methods: {
 getProducts (prodId) {
     this.$store.dispatch('FETCH_PRODUCTS', prodId)
  }
}

Code in vuex store:

import myApi from '../../gateways/my-api'
const state = {
  products: []
}

const actions = {
  FETCH_PRODUCTS: (state, prodId) => {
    myApi.get('products?id=' + prodId).then(response => state.commit('SET_PRODUCTS', response))
  }
} 

// mutations
const mutations = {
  SET_PRODUCTS: (state, data) => {
    state.products = Object.assign({}, response.data)
  }
}

const getters = {
}

export default {
  state,
  mutations,
  actions,
  getters
}

Solution 2:

Short answer: considering the Jackblog example - you just need to import "api" from the component, and use the API directly. Do not import actions. In Messages.vue, forget about the store. You don't need the actions layer, which is tied to the store. You just need the API.


Long answer: in a project we have the following

  1. An Ajax library wrapper providing a function named remote which accepts two parameters: a string and an object. The string tells what we're trying to achieve (eg, "saveProductComment") and the object is the payload (parameters' names and values, to be sent to server).

  2. Each app module may contain a "routes.js" file, which maps the "string" above with a route configuration. For example: saveProductComment: 'POST api/v1/products/{product_id}/comment'

Note: I am not using the term "app module" for a single .js or .vue file which is treated as a "module" by NodeJS or Webpack. I am calling "app module" a full folder containing app code related to a specific domain (examples: "cart" module, "account" module, "comments" module, et cetera).

  1. From anywhere, we can call remote('saveProductComment', { product_id: 108, comment: 'Interesting!' }), and it returns a Promise. The wrapper uses the route configuration to build the proper request, and it also parses the response and handles errors. In any case, the remote function always returns a Promise.

  2. Each app module may also provide its own store module, where we define initial state, mutations, actions and getters related to the module. We are using the term "Manager" for the state management code. For example, we can have a "commentsManager.js" file, providing the store module to take care of "comments".

  3. In the Manager, we use the remote function to make the API calls inside Vuex actions. We return the Promise from remote, but we also attach to it the callback which handle the results. In the callback, we call the mutation functions to commit the results:


newProductComment ({ commit }, { product, contents }) {
    return remote('saveProductComment', {
        product_id: product.id,
        comment: contents
    })
    .then(result => {
        commit('SOME_MUTATION', result.someProperty)
    })
}

Now, if we want to use same API call outside Vuex context, but directly inside a component, we just need to use similar code in a Vue component method. For example:

export default {
    name: 'myComponent',
    data: () => ({
        contents: '',
        someData: null
    }),
    props: ['product'],
    methods: {
        saveComment () {
            remote('saveProductComment', {
                product_id: this.product.id,
                comment: this.contents
            })
            .then(result => {
                this.someData = result.someProperty
            })
        }
    }
}

In terms of app structure, what really matters for us is:

  • having the app properly divided into distinct concerns; what we call "app modules"; one module for each specific thing

  • we have a "modules" folder containing a folder for each "app module"

  • inside a specific "app module folder", we have the routes config at routes.js, mapping the remote function first parameter to a route configuration; our custom code pick the HTTP method, interpolates the URL, do all kinds of fancy stuff, properly suited to our needs; but anywhere in the remaining of the app code, we just use it in that simple way: remote('stuffNeededToBeAccomplished', { dataToAccomplishTheNeed })

  • in other words, the heavy work is in the mapping and in the Ajax library wrapper (you can use any Ajax library for the actual requests); note that this is totally independent of using Vue / Vuex

  • we have the Vuex store also divided in modules; usually, in an app module, we have the corresponding store module using the routes defined there

  • in the main entry point, we import the app modules; the index.js of each module takes care of registering both the routes in the Ajax wrapper and the store modules in Vuex (so , we just need to import, and take no further action, to have routes available for Ajax and the store modules available in Vuex)