VueJS

Computing Application State in Vue 3

Lindsay Wardell
6 min read
Computing Application State in Vue 3
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Introduction

We've all been there before- working on an application, and suddently we need to determine what state something is in. Maybe it's whether the form has been submitted already, or the class a certain element should have. You may be tempted to set that value to a variable, and move on. What's the harm, right?

It turns out that there are a number of reasons this could be a problem. Your state could be out of date due to a change from a different function. You could cause an 'impossible state', or a state in the UI that was never intended by the developer. And at the very least, your code is more imperative, meaning that you as the developer are having to write and maintain more lines of code. It's almost like manually juggling all the values in your application - what happens if you drop one?

Luckily, Vue provides a solution for this - the computed property. With the computed property (or Composition API method), we can perform calculations like we described above by declaring them and getting a readonly, reactive ref to use in our application. Vue is able to determine when a dependency in the computed property has changed, and recalculate its result. We can then use these calculated values as if they were another variable, and use them in our template and logic with ease.

Setting up our example

Let's start with a common example: You have been tasked with building a form that accepts a name, email, and comments. We want to track the number of characters a user has entered, and allow them to submit the form. Below is an example of this form:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea
        id="comment"
        v-model="formState.comment"
        @input="updateCharacterCount"
      />
      {{ characterCount }} character(s)
    </label>
    <button>Submit Feedback</button>
  </form>
</template>

<script>
import { reactive, defineComponent, ref } from "vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });

    const characterCount = ref(0);

    const updateCharacterCount = (e) => {
      characterCount.value = e.target.value.length
    };

    const onSubmitFormHandler = () => {
      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => console.log(res))
        .catch((err) => console.log(err));
    };

    return {
      formState,
      characterCount,
      updateCharacterCount,
      onSubmitFormHandler,
    };
  },
});
</script>

The above form provides the basic functionality that we need. But there are a few things missing:

  • We're just logging out messages when the API comes back. We should report the error or success state to the user.
  • We probably shouldn't allow users to submit the form without filling out the fields.
  • We also shouldn't allow users to submit the form while their submission is being processed. We don't want to receive duplicate form entries.

With this in mind, let's rewrite our code. The template is the same, but we'll need to track the form's state - whether it has been submitted or had an error, and whether the user can click the submit button. Sounds simple enough, so let's add some booleans - submitting, hasError, hasSuccess. That should handle the state.

We don't want the user to have to click a "Validate" button, of course - that would be frustrating. So let's use the watch Composition API method, and calculate whether to show the submit button.

Below is our updated code:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea id="comment" v-model="formState.comment" @input="updateCharacterCount" />
      {{ characterCount }} character(s)
    </label>
    <button :disabled="submitting || !formReadyToSubmit">
      Submit Feedback
    </button>
  </form>
  <div>
    <template v-if="hasError">Something went wrong!</template>
    <template v-if="hasSuccess">Submitting successfully!</template>
  </div>
</template>

<script>
import { reactive, ref, watch, defineComponent } from "vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });

    const characterCount = ref(0);

    const submitting = ref(false);
    const hasError = ref(false);
    const hasSuccess = ref(false);

    const formReadyToSubmit = ref(false);

    const updateCharacterCount = (e) => {
      characterCount.value = e.target.value.length
    };

    const validateHasInput = () => {
      formReadyToSubmit.value =
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0;
    };

    watch(
      () => ({ ...formState }),
      () => {
        validateHasInput();
      },
      {
        deep: true,
      }
    );

    const onSubmitFormHandler = () => {
      hasError.value = false;
      hasSuccess.value = false;
      submitting.value = true;

      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => {
          console.log(res);

          hasSuccess.value = true;
          submitting.value = false;
        })
        .catch((err) => {
          console.log(err);

          hasError.value = true;
          submitting.value = false;
        });
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount,
      updateCharacterCount
    };
  },
});
</script>

The way this is written works, but there are a number of issues:

  • In the onSubmitFormHandler, we are manually resetting each status to where it should be.
  • In addition, the fact that we are using multiple booleans to track our form's state means that we could end up in an impossible state, where both "submitting" and "hasError" are true. That could lead to unexpected, and unpredictable, user experiences.
  • The watcher does the job of recalculating whenever the formState is changed, but we're still having the manually track this value.
  • We're still manually handling the input event on the comments field to get the character count.

Most importantly, it's going to be much harder to refactor this going forward, because it's harder to determine what is going on. Is there a relationship between "hasError" and "hasSuccess"? What is calling "validateHasInput"?

Implementing Computed Property

Let's upgrade our application using the computed Composition method now. First, we'll replace the boolean states with a single state, and use computed properties to determine what state we are in. We can also use a computed property to get the character count, and remove that extra event on the comment textarea. Finally, we'll move the validateHasInput into its own computed property.

Here's the updated form:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea id="comment" v-model="formState.comment" />
      {{ characterCount }} character(s)
    </label>
    <button :disabled="!formReadyToSubmit">Submit Feedback</button>
  </form>
  <div>
    <template v-if="hasError">Something went wrong!</template>
    <template v-if="hasSuccess">Submitting successfully!</template>
  </div>
</template>

<script>
import { reactive, ref, computed, defineComponent } from "vue";
import axios from "axios";

const Status = {
  IDLE: "IDLE",
  SUBMITTING: "SUBMITTING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
};

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });
    const characterCount = computed(() => formState.comment.length);

    const status = ref(Status.IDLE);

    const submitting = computed(() => status === Status.SUBMITTING);
    const hasError = computed(() => status === Status.ERROR);
    const hasSuccess = computed(() => status === Status.SUCCESS);

    const formReadyToSubmit = computed(
      () =>
        !submitting.value &&
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0
    );

    const onSubmitFormHandler = () => {
      status.value = Status.SUBMITTING;

      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => {
          console.log(res);

          status.value = Status.SUCCESS;
        })
        .catch((err) => {
          console.log(err);

          status.value = Status.ERROR;
        });
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount
    };
  },
});
</script>

We have now refactored to use the computed property. What does that give us?

  • The boolean values (submitting, hasError, hasSuccess) are no longer being set imperatively. They are being calculated based off of a status variable. If we were using something like Typescript, we could force the type of status to be a certain subset, but for now, we are using an object with a few states - including IDLE.
  • The character count is now being calculated off of the length of the comment string, rather than checking the length from the emitted event. This means that if we change the value of formState.comment programmatically, our character count is up to date without us having to change anything.
  • In the onSubmitFormHandler, we set which status our form is in as we go. We aren't having to set each status individually, which means we don't have the possibility of impossible states.

Using Getters in Vuex

Our application is in a much better state now, but there is still room to improve. One way we can better encapsulate our logic is by utilizing Vuex for managing the state of the form submission. Luckily, we can implement our computed logic in Vuex pretty easily with getters. In Vuex, getters fill the same role as computed properties in single-file components.

Let's move our API logic out of the component, and into Vuex:

import { createStore } from "vuex";
import axios from 'axios';

export const Status = {
  IDLE: "IDLE",
  SUBMITTING: "SUBMITTING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
};

export default createStore({
  state: {
    status: Status.IDLE
  },
  getters: {
    getStatus: state => state.status,
    submitting: state => state.status === Status.SUBMITTING,
    hasError: state => state.status === Status.ERROR,
    hasSuccess: state => state.status === Status.SUCCESS
  },
  mutations: {
    'SET_STATUS': (state, status) => state.status = status
  },
  actions: {
    submitForm: ({ commit }, payload) => {
      commit('SET_STATUS', Status.SUBMITTING);

      axios
        .post("http://localhost:3000/api/comment", payload)
        .then((res) => {
          console.log(res);

          commit('SET_STATUS', Status.SUCCESS);
        })
        .catch((err) => {
          console.log(err);

          commit('SET_STATUS', Status.ERROR);
        });
    }
  }
});

In Vuex, we create a store, which is then plugged into our app. This store contains our state (the submission status), a single mutation to handle updating the state, and an action for submitting the form. We then have our four computed properties, which match the three we had previously as well as getting the raw state.

By using Vuex, the state of our form submission is disconnected from the template. This can be useful when building out larger applcations, so that your components remain focused on the user experience, and the logic is handled within your global state management. Here's what our updated component's logic looks like:

import { reactive, computed, defineComponent } from "vue";
import { useStore } from "vuex";

export default defineComponent({
  setup() {
    const store = useStore();
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });
    const characterCount = computed(() => formState.comment.length);

    const submitting = computed(() => store.getters.submitting);
    const hasError = computed(() => store.getters.hasError);
    const hasSuccess = computed(() => store.getters.hasSuccess);

    const formReadyToSubmit = computed(
      () =>
        !submitting.value &&
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0
    );

    const onSubmitFormHandler = () => {
      store.dispatch("submitForm", formState);
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount,
    };
  },
});

In our component, we are now importing useStore in order to access our Vuex store. The component then makes an API call via a dispatch, which calls our action. Neat!

Using Get/Set with Computed Properties

One more nice feature of computed properties is how they interact with ES5 getters and setters. If you aren't aware, with ES5 you can add a function to an object that is either a get() or a set(val). This function is not invoked like a normal function, but instead, whenever the value is read or assigned to. For example:

console.log(this.name) // Getter

this.name = "Tim"; // Setter

With computed properties, we can leverage this system to build additional functionality. Let's say that you want to add a reset function to the form. One potential way to do that could be like this:

const currentStatus = computed({
  get: () => store.getters.getStatus,
  set: (val) => store.commit("SET_STATUS", val),
});

const resetFormHandler = () => {
  formState.name = "";
  formState.email = "";
  formState.comment = "";

  currentStatus.value = Status.IDLE;
};

With the Composition API, if you pass an object in as the first parameter (rather than a function), you can use the get and set keys to make your own custom getter and setter. This way, rather than making the function call directly to store.commit, we can simply set the currentStatus to the status we want.

This ability to set to computed properties is especially useful when using v-model on a custom component. Below is an example where this can help to model a custom input.

<template>
  <input v-model="inputValue">
</template>

<script>
export default {
  emits: [ 'update:modelValue' ],
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  computed: {
    inputValue: {
      get() {
        return this.modelValue;
      },
      set(val) {
        this.$emit('update:modelValue', val);
      }
    }
  }
}
</script>

By leveraging getters and setters in your computed properties, you can cut down on the amount of code you need to write in order to perform bindings between components and Vuex. You can also add additional logic, such as validation or data cleanup. Just make sure that you aren't overburdening your setters- if they start to get large, you might need to use a separate function anyway.

Conclusion

Computed properties can help ensure that your applications are easy to maintain and understandable. By leveraging a computed property, rather than manually assigning values, you allow the framework to do the heavy lifting for you. Computed properties are great for a number of complex tasks, such as:

  • Form status
  • API calls
  • Formulaic calculations (temperature conversion, weight conversion)
  • State-based CSS classes
  • Determining which state to render your component in.

Keep in mind - computed properties should not cause side effects. If you are writing a computed property that needs to alter your state, it should probably be a watcher.

Take a look at your own applications, and see where using a computed property could be beneficial. Have fun!

About the author

Lindsay Wardell

Lindsay Wardell

Software Engineer

Lindsay is a software engineer at This Dot Labs, and a host on the podcast Views on Vue. She loves learning about new languages and tools, and sharing that knowledge with the community.

Keep reading

View all posts →

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Unleash the power of 3D in your Vue.js projects with Tres.js! The future of immersive web experiences is here. #Vuejs #3Ddevelopment...

Mattia Magi5 mins
Vue3D

3 VueJS Component Libraries Perfect for Beginners

For developers checking out VueJS for the first time, the initial steps are overwhelming, particularly when setting up projects from square one. But don’t worry! The VueJS ecosystem offers a plethora of remarkable component libraries, easing this early obstacle. These three libraries are pre built toolkits, providing beginners with the means to kickstart their VueJS projects effortlessly. Let’s take a look! Quasar Quasar is among the most popular open source component libraries for Vue.js, offering a comprehensive set of ready to use UI components and tools for building responsive web applications and websites. Designed with performance, flexibility, and ease of use in mind, Quasar provides developers with a wide range of customizable components, such as buttons, forms, dialogs, and layouts, along with built in support for themes, internationalization, and accessibility. With its extensive documentation, active community support, and seamless integration with Vue CLI and Vuex, Quasar empowers developers to rapidly prototype and develop high quality Vue.js applications for various platforms, including desktop, mobile, and PWA (Progressive Web Apps). PrimeVue PrimeVue is a popular Vue.js component library offering a wide range of customizable UI components designed for modern web applications. Developed by PrimeTek, it follows Material Design guidelines, ensuring responsiveness and accessibility across devices. With features like theming, internationalization, and advanced functionalities such as lazy loading and drag and drop, PrimeVue provides developers with the tools to create elegant and high performing Vue.js applications efficiently. Supported by clear documentation, demos, and an active community, PrimeVue is an excellent choice for developers seeking to streamline their development process and deliver polished user experiences. Vuetify Vuetify is a powerful Vue.js component library that empowers developers to create elegant and responsive user interfaces with ease. Built according to Google's Material Design guidelines, Vuetify offers a vast collection of customizable UI components, ranging from buttons and cards to navigation bars and data tables. Its comprehensive set of features includes themes, typography, layout grids, and advanced components like dialogues and sliders, enabling developers to quickly build modern web applications that look and feel polished. With extensive documentation, active community support, and ongoing development, Vuetify remains a top choice for Vue.js developers seeking to streamline their workflow and deliver visually stunning user experiences. For newcomers venturing into Vue.js, the initial setup might seem daunting. Thankfully, Vue.js offers a variety of component libraries to simplify this process. Quasar, PrimeVue, and Vuetify are standout options, each providing pre built tools to kickstart projects smoothly. Whether you prefer Quasar's extensive UI components, PrimeVue's Material Design inspired features, or Vuetify's responsive interfaces, these libraries cater to diverse preferences and project requirements. With their clear documentation and active communities, these libraries empower developers to start Vue.js projects confidently and efficiently, enabling Vue developers to create polished user experiences....

2 mins
VuetifyVueQuasar

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how <Suspense> and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly...

Jesús Padrón6 mins
VueWeb PerformanceUXJavaScript

Getting started with Vitepress

Create your static blog site using Vitepress. Setup and configure your site in minutes by following this step-by-step tutorial....

Simone Cuomo12 mins
VueViteVitepress