Peter Ekene Eze Learn, Apply, Share

Refactoring your Vue 2 apps to Vue 3

7 min read 2037

Refactoring your Vue2 apps to Vue3

Vue 3 provides developers with an alternative and better way to build Vue applications. The introduction of the composition API and composition functions in Vue 3 has made code organization and reusability a breeze.

What to expect

In this tutorial, we will explore the new features in Vue 3. We will also refactor Brad Traversy‘s open-source Vue 2 application to Vue 3. In the process, you will learn how to use the new Vue 3 features, including the composition API and composition functions. Lastly, you will also learn how to use the new Vuex 4 in a Vue 3 application.

What is not covered

  • Styling β€” We won’t cover the CSS properties used in this project in detail. Although it is available on the project repository for you
  • Introduction to Vue 3 β€” This tutorial assumes familiarity with Vue 3’s composition API and reactivity fundamentals
  • TypeScript β€” We won’t cover how Vue 3 works within TypeScript in the scope of this project

Prerequisites:

  • Basic understanding of JavaScript and Vue.js will improve your experience in this tutorial
  • If you would like to build along, install any code editor of your choice or get Visual Studio Code
  • This tutorial assumes that you have a basic understanding of both Vue 2 and Vue 3
  • This is not an introduction to Vue 3 tutorial. It will be most helpful to Vue 2 users with minimal understanding of Vue 3 looking to refactor Vue 2 projects to Vue 3

What’s wrong with Vue 2? πŸ€”

The inspirations for Vue 3 come from the existing limitations of Vue 2. Some of them are:

  • Components become less readable and consequently less maintainable as they grow larger
  • All available code reusability patterns in Vue 2 have unsolved bottlenecks
  • Vue 2 has limited TypeScript support
  • Find more reasons here

Say hi to Vue 3 πŸ–πŸ½

Vue 3 aims to solve the existing limitations of Vue 2. With the composition API in Vue 3, you can organize component code based on logical concerns, as opposed to component options. This was problematic in Vue 2 as pieces of code could get spread out in multiple components options. This makes Vue 3 particularly simpler to read and maintain.

Getting started

Let’s start with the Vue 2 project created by Brad Traversy. The project is a todo application where you can perform some of the following actions:

  • Add todos
  • Delete todos
  • Mark todos as completed
  • Filter todos and
  • Hide/show todos

vue todos

Within the project, the todos are fetched from a Typicode API endpoint. All the actions performed on the data are happening on the backend. This is an interesting project that will be just right to expose how Vue 3 offers an alternative approach to building Vue applications.

Create a Vue application

First, you need to install the latest version of Vue CLI v4.5 with the command below:

yarn global add @vue/cli@next 
#OR
npm install -g @vue/cli@next

The process of creating Vue applications hasn’t changed. If you have the Vue CLI installed, run the command below to create a new Vue project called Vue3-todo :

vue create vue3-todo

Follow the terminal prompts and select the Vue 3 preset to complete the setup process. Be sure to select Vuex as part of the features needed for the project or install it later.

We made a custom demo for .
No really. Click here to check it out.

Some noticeable changes in the project files:

Vue 2 uses the new Vue() method to bootstrap new Vue applications. Vue 3 uses the createApp method. If you are wondering why this change is necessary, it’s because the old syntax made it possible for global configurations to permanently mutate global state and that can be problematic for a number of reasons. Here’s a quick look at the main.js file:

main js file

Vuex 4 now has a createStore() method that conforms with Vue 3 standards. With the new updates in Vuex 4, you can create a Vuex store in a few lines of code. Here’s the current implementation:

Vue store created in a few lines of code

Fetch todos

From the implementation on the Vue 2 we are refactoring, when the app loads, we dispatch an action that fetches todos from a Typicode endpoint. Let’s set up an action in the Vuex store to handle that:

// @/store/index.js
import { ACTION_TYPES } from "../constants/action-types";
import Vuex from "vuex";
import Axios from "axios";
export default Vuex.createStore({
  state: {
    todos: [],
  },
  mutations: {
    [ACTION_TYPES.fetchTodos]: (state, todos) => (state.todos = todos),
  },
  actions: {
    onFetchTodos: async ({ commit }) => {
      const response = await Axios.get(
        "https://jsonplaceholder.typicode.com/todos"
      );
      commit(ACTION_TYPES.fetchTodos, response.data);
    },
  },
});

Here, the onFetchTodos action fetches a list of todos from Typicode and commits the fetchTodos mutation. The mutation then updates the todos array in the state object. We’ll dispatch this action in the Todo.vuecomponent which we’ll create shortly.

You may have noticed that we imported ACTION_TYPES from a nonexistent file. Let’s create it. In the root folder, create a src/constants/action-types.js file and update it like so:

// @/constants/action-types.js
export const ACTION_TYPES = {
  fetchTodos: "fetchTodos",
  addTodo: "addTodo",
  deleteTodo: "deleteTodo",
  updateTodo: "updateTodo",
};

The action types we specified in the file above will remain constant across the project and help us maintain naming consistency within the app. Next, let’s create the src/components/Todos.vue file that will dispatch the onFetchTodos action and display the returned todos:

// @/components/Todos.vue
<template>
  <div>
    <div class="todos">
      <div v-for="todo in todos" :key="todo.id" class="todo">
        <p>{{ todo.title }}</p>
      </div>
    </div>
  </div>
</template>
<script>
import { useStore } from "vuex";
import { computed, onMounted } from "vue";
export default {
  name: "Todos",
  setup() {
    const store = useStore();
    const todos = computed(() => store.state.todos);
    onMounted(() => {
      store.dispatch("onFetchTodos");
    });
    return {
      todos,
    };
  },
};
</script>

If this is your first encounter with Vue 3, this component will look unfamiliar. I recommend reading the composition API docs first before continuing with this tutorial. Let’s quickly go over some of the new (Vue 3 specific) things in this component:

The setup() function

Vue 3 uses the composition API which makes it possible to organize all our components logic inside a setup() function that returns the piece of data required in the template. This particular feature makes organizing components by logical concerns possible. That is because we can have lifecycle methods, computed properties, state data, etc. all within the setup() function.

The onMounted() hook

The onMounted() hook in Vue 3 is the equivalence of the mounted() component property in Vue 2. It takes in a callback function that executes when the hook is called by the component:

onMounted(() => {
  store.dispatch("onFetchTodos");
});

The computed() method

The computed() method in Vue 3 is the equivalent of the computed component property in Vue 2. It takes in a getter function that performs a specific task and returns a reactive reference. The reference is an immutable object for the returned value from the getter:

const todos = computed(() => store.state.todos);

Finally, notice that we returned todos in the setup() function. This is how we expose data in the function that we want to use in the template. At the moment, the app looks like this on the browser:

todo list

Since we are doing a refactor, it would’ve been best to capture Vue 2 component side by side. I tried it and it made the tutorial very large and inadvisably lengthy. So I will link the equivalent Vue 2 component instead so you can compare the changes on your own.

Add todos

Since we are using Vuex, we will need an action and a mutation that will update the todos state. To implement that, update the store with the snippet below:

// @/store/index.js

import { ACTION_TYPES } from "../constants/action-types";
import Vuex from "vuex";
export default Vuex.createStore({
  state: {
    todos: [],
  },
  mutations: {
    // ...
    [ACTION_TYPES.addTodo]: (state, todo) => state.todos.unshift(todo),
  },
  actions: {
    // ...
    onAddTodo: async ({ commit }, title) => {
      const response = await Axios.post(
        "https://jsonplaceholder.typicode.com/todos",
        { title, completed: false }
      );
      commit(ACTION_TYPES.addTodo, response.data);
    }
  },
});

The onAddTodo action takes in the state and the new todo that will be added to the existing todos and commits the addTodo mutation. The addTodo mutation updates the todos array with the new todo passed in from the onAddTodo action.

Next, in the components folder, create a components/AddTodo.vue file. This component will render a form that will allow users to add a new todo to the existing list:

// @/components/AddTodo.vue
<template>
  <div>
    <form>
      <input type="text" v-model="title" placeholder="Add to do ..." />
      <input v-on:click="addNewTodo" type="submit" value="Add" />
    </form>
    <br />
  </div>
</template>
<script>
import { ref } from "vue";
import { useStore } from "vuex";
export default {
  name: "AddTodo",
  setup() {
    const store = useStore();
    const title = ref("");
    const addNewTodo = (e) => {
      e.preventDefault();
      store.dispatch("onAddTodo", {
        title: title.value,
      });
      title.value = "";
    };
    return {
      title,
      addNewTodo,
    };
  },
};
</script>

This component is similar to the Todos component. The only new thing here is the ref function on line 17. With Vue 3, we can make variables reactive anywhere in the component using the ref function. Consider this snippet:

// Declaration    
const title = ref("");

Reactivity with ref

The snippet above creates a reactive title variable with an initial value of " " signifying an empty String. To read the value of the variable, you do title.value. You can read more about Vue 3 refs here.

Finally, we return the title variable, along with the addNewTodo in the setup() function to make it available for use in the template. If you check the app on the browser, you should be able to add todos:

add todos to board

Once again, if you appreciate comparing this component directly with the Vue 2 alternative, I encourage you to take a look at it and review the changes.

Filter todos

Typicode returns an array of 200 todos when you call the fetch todos endpoint. Consequently, they provide an endpoint to filter the number of todos you can fetch at a time. Let’s create a filter component that allows a user to filter the number of todos they want to fetch. Create a src/components/FilterTodo.vue file in the root and update it with the snippet below:

// @/components/FilterTodo.vue
<template>
  <div>
    <form @submit="filterTodos">
      <input type="number" v-model="limit" placeholder="Add to do ..." />
      <input @click.prevent="filterTodos" type="submit" value="Filter" />
    </form>
    <br />
  </div>
</template>
<script>
import { ref } from "vue";
import { useStore } from "vuex";
export default {
  name: "FilterTodos",
  setup() {
    const limit = ref(200);
    const store = useStore();
    const filterTodos = () => {
      store.dispatch("onFilterTodos", limit.value);
    };
    return {
      limit,
      filterTodos,
    };
  },
};
</script>

This component presents an input form to collect the number of todos you want to filter for. When submitted, the onFilterTodos action is dispatched. This action commits the fetchTodos mutation which then updates the todos state. Let’s update the Vuex store with the onFilterTodos action like so:

// @/store/index.js

import { ACTION_TYPES } from "../constants/action-types";
import Vuex from "vuex";
export default Vuex.createStore({
  state: {
    todos: [],
  },
  mutations: {
    // ...
    [ACTION_TYPES.fetchTodos]: (state, todos) => (state.todos = todos),
  },
  actions: {
    // ...
    onFilterTodos: async ({ commit }, limit) => {
      const response = await Axios.get(
        `https://jsonplaceholder.typicode.com/todos?_limit=${limit}`
      );
      commit(ACTION_TYPES.fetchTodos, response.data);
    },
  },
});

The onFilterTodos action makes an API call to the Typicode filter endpoint with the value passed in from the component. This value determines the number of todos we get back from Typicode. Then we commit the fetchTodos mutation which updates the todos state with the response data. Let’s try this out on the browser:

fetchtodos

Finally, for comparisons sake, this is the Vue 2 equivalence of these components that we just refactored to Vue 3. We used a form here to handle user input and to keep it simple.

Project repo

This project is available on Github. The complete project contains some features that we couldn’t cover in this tutorial. Some of which are:

  • Deleting todos
  • Updating todos
  • Marking todos as completed
  • Toggling todos visibility

Conclusion

In this project, we’ve rebuilt a standard Vue 2 application with Vuex into a brand new Vue 3 app. In the process, we looked at some of Vue 3’s features like the composition API, composition functions, and Vuex 4. Vue 3 is still changing so some things might be different at the time you read this so keep yourself updated via the official Vue 3 documentation. You can find the code for the project on my repository here.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Experience your Vue apps exactly how a user does

    Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket. https://logrocket.com/signup/

    LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

    The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

    Modernize how you debug your Vue apps - .

    Peter Ekene Eze Learn, Apply, Share

    Leave a Reply