DEV Community

Cover image for Managing the state in Vue (responsibly)
Jesús Escamilla for Htech

Posted on

Managing the state in Vue (responsibly)

Summary:

  • Learn different ways to manage state.
  • Review some examples.
  • Evolution.
  • Recommend a good structure.
  • Conclusions.

In this post, we will be covering the most common ways to manage state in a Vue app. I will share some of my personal experience and the evolution of our implementation in our workplace, which allows us to structure them depending on our objectives.

Here we go!!!

A lot of different ways to manage state

In summary, we can classify them in:

  • Local state inside component.
  • Global state in the window object.
  • Global state with vuex.
  • Shared observables.
  • Shared reactives.

Examples

The examples are written for Vue 2 or Vue 3 and some code may be omitted, the objective is to specify the parts about state management. Of course I'm here to help you if something is unclear.

Local state inside component

Vue 2

export default {
  data() {
    return {
      text: ''
    }
  },
  methods: {
    updateText(value: string) {
      this.text = value;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Vue 3

import { ref } from 'vue';

export default {
  setup() {
    let isLoading = ref(false);

    const setLoading = (status: boolean) => {
      isLoading.value = status;
    }

    return {
      isLoading,
      setLoading
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

For both Vue versions the example is essentially the same, shown in two different ways. Basically a var updated by a method.

That could be the simplest form of state management; you may consider that updating the variable directly is better, but I recommend using a method. Primarily because a method is better for understanding the objective and possible values (especially if you are using typescript).

As a recommendation, try and maintain the most amount of data separated, in its own component, don't share data unless you truly need to.

Global state in the window object

Have you used something like this?

window.global.config= {};
Enter fullscreen mode Exit fullscreen mode

Spoiler, I wouldn't recommend doing this in this way for most cases because there are no controls in place. Although, in a simple app, it could be a fast way to share some high level data.

Another popular use in Vue is:

window.global.eventBus = new Vue();
Enter fullscreen mode Exit fullscreen mode

We'll leave communication between components for another post, here we will talk within the context of state management.

I've decided to include this here because I consider this a shortcut if you only need to share a small amount of data, as a method or an utility. Keep in mind that this is for small amounts of data, for larger amounts of data, please consider the following option:

Global state with vuex

Now a general example for a posts vuex module:


import * as types from '../mutation-types';

const state = {
  isLoading: false,
  posts: []
};

const getters = {
  isLoading: () => state.isLoading,
  posts: () => state.posts,
  unreadedPosts: () => state.posts
    .filter((post) => post.readed === false)
};

const actions = {
  setLoading({ commit }, status) {
    commit(types.SET_LOADING, status);
  },
  loadPosts({ commit }, posts) {
    commit(types.LOAD_POSTS, posts);
  }
};

const mutations = {
  [types.SET_LOADING](_, isLoading) {
    state.isLoading = isLoading;
  },
  [types.LOAD_POSTS](_, posts) {
    state.posts = posts;
  }
};

export default {
  state,
  getters,
  actions,
  mutations
};

Enter fullscreen mode Exit fullscreen mode

And the Vue 2 component implementation

import { mapGetters, mapActions } from 'vuex';
import api form './api';

export default {
  computed: {
    ...mapGetters(['posts'])
  },
  methods: {
    ...mapActions(['loadPosts']),
    async getPosts() {
      const posts = await api.getPosts();
      this.loadPosts(posts);
    }
  }
Enter fullscreen mode Exit fullscreen mode

As you can see I added a basic api method to get posts and send them to the state.

I commonly see people make the api request in the vuex action, I used to do it that way too, but now I highly recommend separating the concerns no matter how simple the code might seem to be. You'll thank me later, as the code gets larger.

Also remember that executing an action, calling a mutation, updating a state and reading getters make up a beautiful cycle that you need to respect and keep as simple as possible.

So, if you need a vuex, keep actions limited to (at most) validating the entrance of data, never asynchronous or unpredictable methods. Keeping mutations only to update the state and getters only to provide data or basic filtering/formatting.

Finally, when exactly do you need vuex? Anytime you need to retrieve data that all the application needs, within different views and modules.

Shared observables

As simple as this:

import Vue from 'vue';

export default Vue.observable({
  isOpen: true
});

Enter fullscreen mode Exit fullscreen mode

A Vue observable exposes a reactive object that could be accessed and updated in different components such as:

import sidenavState from './sidenavState';

export default {
  computed: {
    isOpenSidenav() {
      return sidenavState.isOpen;
    }
  },
  methods: {
    setSidenavStatus(status: boolean) {
      sidenavState.isOpen = status;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It's not a law, but I highly recommend reading the observable by a computed property (which becomes useful later if you need to listen to it by a watch) and updating by a method (remember the update methods I listed in the examples).

The advantage here is that other components can read from the same observable and get updated with the same data.

In principle it’s a simpler alternative to vuex, but without actions and mutations.

In general, remember to add methods or computed properties to keep it manageable.

Also, I recommend vue observables for small-medium things, like data shared between views in the same module, but with a data nesting or no direct relation that allows props/emits communication.

Shared reactives

In Vue 3 composition api, the evolution of observables are here:

import { reactive, readonly } from 'vue';

const state = reactive({
  isMovile: false
});

const setMovile = (status: boolean) => {
  state.isMovile = status;
};

export default {
  state: readonly(state),
  setMovile
};
Enter fullscreen mode Exit fullscreen mode

In this example, we keep them if the app is in mobile state, it can be used for taking responsive decisions or hiding/showing components.

Finally, expose a method to update and, woah, a readonly helper to help us prevent someone from directly updating the state.

That is the main advantage I see in comparison to observables. I really recommend exposing the state as a readonly and update by the methods I've described.

I see a lot of potential in that structure and recommend it thoroughly for all application levels and replacement of observables if you're migrating from Vue 2 to Vue 3.

An important recommendation I can give you is to not use only one state file, it's much better to divide them into single concerns and locate them in the corresponding app level (component, view, module or app).

Also remember to keep exposed methods simple, never asynchronous, complex stuff or business logic.

Evolution from vuex to reactives

I started (like many others) with a flux styles strategy, coming from react and redux, in vue (2.x), with vuex as expected.

Our projects started very small, so we used vuex for everything, what could go wrong?

We were thinking in a lineal flow, where data is generated as the user progresses through to the final view. We considered 4 or 5 views, simple enough, one vuex module per view.

All was well at first, but as new views were added, alternate flows, processes that came in and out of external sites (say goodbye to your saved state), it all became a nightmare to maintain.

That is when we started exploring observables and even more recently reactives, introducing Vue 3 to our projects.

We found that major cases really don't need much data to be shared globally so vuex was omitted in new projects. Now, Reactive stores have been introduced, they are simpler because they can be divided on each app level that requires it. Also because most data only needs to be in its respective component.

Structuring

Now the best part, how to organize this? I propose a hierarchical way, so it can be easy to understand the scope of the state.

This is not limited to one specific state management method, but I will recommend something for each level based on my experience.

Component

Use internal data options, some refs or reactives. Data received by apis lives only in this component and is shared to others by props or events.

If it is a big amount of data, it may be taken out to another file in the same folder level.

View/module

This is similar to Component level, but, think of a view as a classic routed component, and a module as a more complex component (header, menus, etc.). So consider three more possible things:

  • There are more than two nesting levels.
  • So many props are required for many sub components.
  • Some get/set methods are required to share too.

Using Observables or reactives in a specific file at the view/module level is a good option.

Between views/modules

First of all, if we talk about sub-views or low-level modules, the escenario may be similar to the previous level. Here, we are talking mostly about first level views or modules, and between them we need to share state.

Here, we use an app level shared state. It may be created with reactives or vuex modules but remember to organize based on objectives or entities, not in view names or module names (if this is the case, go back to previous level).

App

If you got here, we can talk about data that is not related to any view, module, or component, like some authentication status, or user configurations may be.

Reactives or Vuex are recommended but the main difference with previews level is, as you can imagine, that the state managed here is independent of all and, by definition, may be useful for each part of the application.

Top to down

At this point you might say, if I have a state value in App level, I can access them in any component, but that is a bad idea. Going back to the hierarchical proposal, low level components must get data only from props.

For example, a view component can access data from app level state and share it to children by props. This can be done easily and ensures that the children remain independent, no need to know more about higher levels, while making them more shareables.

Bonus: Vue reactives vs vuex

I think a tool is only as strong as the user. So I wouldn’t say no to vuex. In our projects we found there are very limited cases to use it because the composition api utilities and a good structure as shown before give us a simpler and more maintainable way.

If you do, in fact, need all the power and beauty of vuex, remember the previous recommendations.

If you need simplicity and flexibility use the composition api reactive.

Conclusions

Everything I’ve said can be condensed as... the state is something you could have under control, organized, easy-breezy, or you could have a snowball running down hill, about to crush you.

So remember to use the least global state possible, keep it only in its respective components and share it directly by props and events, while maintaining it in specific files within the required level.

Update the state view specific methods to ensure data quality and don't attach low level components to higher levels without reason.

I hope this will help you improve your project structure and keep in order your app state, so that you can have a happier life.

Follow us to get more posts about Vue soon.

Discussion (1)

Collapse
neeshaparez profile image
NeeshaParez

It is somewhat fantastic, and yet checking out some more about this from you. couple break up spell