Boost Your Project with Best Modern Tools for Vue.js Development

Best Tools for Modern Vuejs Projects _ Boost Your Project with Best Modern Tools for Vue.js Development

In today's web development, efficient, modern tooling is a cornerstone of creating healthy, scalable, and maintainable apps. Using the best tools for Vue.js development, no matter the size or purpose of your application can save a whole lot of effort while having a significant impact on quality and performance.  

In this post, we will explore an opinionated list of modern tools that will definitely improve your Vue.js dev experience. From Vue.js project setup tools and state management to build optimization and deployment, these tools will help you create faster, more scalable, and easier-to-maintain applications while future-proofing your development process.

Setting Up Your Project with Vue CLI and Vite

For quite some time now, Vue CLI has been the go-to build tool for Vue projects, and that’s absolutely fine - why not? But at this point, we need to give the new, well maybe not that new as it’s been with us since 2020, alternative - Vite - a chance. With its lightning-fast performance, modern architecture, and developer-focused features, Vite quickly redefines how we approach building and maintaining Vue applications. In short, that’s the answer to why Vite has become the superior choice in recent years. But let’s break it down:

FeatureVue CLIVite
Dev serverWebpackESBuild
Hot Module ReplacementSlower, all files bundled before servingExtremely fast thanks to ESBuild
Configuration complexityExtensive boilerplateSimple boilerplate, extendable if needed
PluginsAll Webpack plugins availableRollup & Vite plugins, growing community support
Typescript supportAdditional configuration requiredBuilt-in support
ESNext supportAvailable but requires polyfillsBuilt-in support

In short, Vite is the faster, more user-friendly alternative, while Vue CLI offers the complexity of Webpack. My advice is to use Vue CLI only for projects that explicitly require the usage of webpack plugins that may be unavailable for Vite and go with the latter in any other case. Nonetheless, let’s compare the setup steps & configuration of both options.

Step-by-Step Setup Guide for Vue CLI and Vite

Setting up your development environment is the first step toward building a successful Vue application. Both Vue CLI and Vite are powerful Vue.js project setup tools, but they cater to different needs. While Vue CLI offers a more traditional, feature-rich setup with Webpack, Vite focuses on speed and simplicity with its modern architecture. Let’s look into the installation steps of both. 

Setting Up with Vue CLI:

Installation:

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

Project creation:

vue create my-vue-app

After which you’ll be prompted to pick desired features, and the project creation will be underway:

Setting Up with Vue CLI project creation

Setting Up with Vite:

There is no need for installation; you can proceed with creating your project.

With Vite’s CLI:

yarn create vite
# OR
npm create vite@latest
Setting up with Vite

Or using a template:

yarn create vite my-vue-app --template vue-ts
setting up with vite image 2

Now, you can simply open the project in your favorite IDE  and enjoy the dev experience!

Alternatively, you can use create-vue, which is a replacement for Vue CLI using Vite under the hood.

Yet another choice to consider is using Nuxt in SSG (static site generation) or SSR (server-side rendering) mode if needed. I highly recommend checking it out, as it has many awesome built-in features that make writing Vue code even more enjoyable.

Webpack vs. Vite: Tools for Modern Vue.js Build Optimization

Let’s not leave the Webpack vs. Vite rivalry behind just yet; we should discuss their different approaches to production builds.

Webpack utilizes its seasoned ecosystem, bundling the entire application and applying optimizations like tree-shaking, code-splitting, and caching. However, additional configurations and plugins are often required to make it work for large, complex projects. If not carefully managed, this usually leads to extended build times. 

In contrast, Vite uses Rollup for production builds, offering similar optimizations but with an easier, out-of-the-box setup. Rollup’s modular system allows efficient tree-shaking and smaller bundle sizes with minimal configuration. While Webpack can be a viable option for highly customized setups or legacy projects, Vite's streamlined workflow and faster build times make it much better for today's, especially as projects scale.

Streamline Vue.js Development with Visual Studio Code

As for IDEs, while VS Code is not without flaws, i.e., some performance issues when it comes to large projects, it still remains one of the most recognized and widely used IDEs; heck, it’s almost considered an industry standard. It is highly customizable, has a clean, user-friendly interface, and loads of useful plugins that will make coding in Vue a breeze.

When enhancing your development workflow in Vue.js, the right set of extensions can make all the difference. Integrating suitable tools into your editor can dramatically boost productivity, reduce errors, and make coding a far more enjoyable experience. So, which extensions should you consider to improve your Vue dev experience? Here’s a breakdown of the top extensions to consider for improving your Vue.js development experience:

Alternatively, you can choose to install the whole Vue Extension Box - which includes Vue-official bundled with other handy plugins:

Advanced State Management with Pinia

At this point, Pinia has been the official global Vue.js state management tool for almost three years—it’s high time to ditch Vuex and reap the benefits of its younger sibling. Pinia offers a simplified API, seamless TypeScript support, and reduced boilerplate. And last but not least, it supports Vue 3’s composition API syntax. In my humble opinion, it is one of the best tools for Vue.js development. What’s there not to love?

Let’s take a look at the key differences between Vuex and Pinia:

AspectPiniaVuex
BoilerplateMinimal setup, no mutations requiredRequires mutations and actions for state updates
TypeScript supportOut of the box, no additional effort neededAll manual typing necessary
API designBoth Options and Composition API are supportedOptions API only, even in Vue3
ArchitectureModular, Tree-shaking supportedModule registration in a centralized structure required - limited Tree-shaking
PerformanceFaster - thanks to direct usage of Vue3’s reactivitySlower - due to additional, custom reactivity layer

Now let’s assume that we want to build a simple to-do app using both tools:

Pinia

Let’s take a look at Pinia first. Below you can see an example of how to create an independent Pinia store module using TypeScript with Vue3’s composition api and how to utilize it in your component.In this module I’ve declared the state as ref’s (tasks, filter), getters as computed properties (filteredTasks) and functions as actions (addTask, removeTask, toggleTask). I also added state persistence by taking advantage of a watcher, local storage and the onMounted lifecycle hook.

// store/tasks.ts

import { defineStore } from "pinia";
import { ref, computed, watch, onMounted } from "vue";

// Define the Task type
interface Task {
  id: number;
  title: string;
  completed: boolean;
}

// Store definition
export const useTaskStore = defineStore("tasks", () => {
  const tasks = ref<Task[]>([
    { id: 1, title: "Task 1", completed: false },
    { id: 2, title: "Task 2", completed: true },
  ]);

  const filter = ref<"all" | "completed" | "incomplete">("all");

  // Getters
  const filteredTasks = computed((): Task[] => {
    if (filter.value === "completed") {
      return tasks.value.filter((task) => task.completed);
    } else if (filter.value === "incomplete") {
      return tasks.value.filter((task) => !task.completed);
    }
    return tasks.value;
  });

  // Actions
  const addTask = (title: string) => {
    tasks.value.push({
      id: Date.now(),
      title,
      completed: false,
    });
  };

  const removeTask = (id: number) => {
    tasks.value = tasks.value.filter((task) => task.id !== id);
  };

  const toggleTask = (id: number) => {
    console.log("id", id);
    const task = tasks.value.find((task) => task.id === id);
    if (task) {
      task.completed = !task.completed;
    }
  };

  // Persist changes to localStorage
  watch(
    tasks,
    (newTasks) => {
      localStorage.setItem("tasks", JSON.stringify(newTasks));
    },
    { deep: true }
  );

  onMounted(() => {
    const storedTasks = localStorage.getItem("tasks");
    if (storedTasks) {
      tasks.value = JSON.parse(storedTasks);
    }
  });

  return { tasks, filter, filteredTasks, addTask, removeTask, toggleTask };
});
// TodoPinia.vue

<template>
  <div class="todo">
    <select v-model="taskStore.filter">
      <option value="all">All</option>
      <option value="completed">Completed</option>
      <option value="incomplete">Incomplete</option>
    </select>

    <ul>
      <li v-for="task in taskStore.filteredTasks" :key="task.id">
        <input
          type="checkbox"
          v-model="task.completed"
          @update="taskStore.toggleTask(task.id)"
        />
        {{ task.title }}
        <button @click="taskStore.removeTask(task.id)">X</button>
      </li>
    </ul>

    <input
      v-model="newTaskTitle"
      @keyup.enter="addTask"
      placeholder="New task"
    />
    <button @click="addTask">Add Task</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { useTaskStore } from "../store/pinia/tasks";

const taskStore = useTaskStore();

const newTaskTitle = ref<string>("");

const addTask = () => {
  if (newTaskTitle.value.trim()) {
    taskStore.addTask(newTaskTitle.value);
    newTaskTitle.value = "";
  }
};
</script>

Vuex

And here goes Vuex. Here, I have recreated basically the same store module as in Pinia. For the purpose of this article, I have simplified things by not creating a root store instance in which I’d have to import the module as you would normally do. This is an extra step for when we need to have more than one Vuex store module. You’ll notice some differences - the clunkier syntax,  more extensive typing, and a dedicated plugin needed for state persistence.

// store/vuex/tasks.ts

import { createStore, Store } from "vuex";

import createPersistedState from "vuex-persistedstate";

import type { InjectionKey } from "vue";

// Define the Task type
export interface Task {
  id: number;
  title: string;
  completed: boolean;
}

// Define the Tasks module state type
export interface TasksState {
  tasks: Task[];
  filter: "all" | "completed" | "incomplete";
}

const persistedTasks = createPersistedState({
  paths: ["tasks"],
});

// Export injection key for better typing with useStore
export const key: InjectionKey<Store<TasksState>> = Symbol();

export const store = createStore<TasksState>({
  plugins: [persistedTasks],
  // State
  state: {
    tasks: [
      { id: 1, title: "Task 1", completed: false },
      { id: 2, title: "Task 2", completed: true },
    ],
    filter: "all",
  },

  // Getters
  getters: {
    filteredTasks(state): Task[] {
      if (state.filter === "completed") {
        return state.tasks.filter((task) => task.completed);
      } else if (state.filter === "incomplete") {
        return state.tasks.filter((task) => !task.completed);
      }
      return state.tasks;
    },
  },

  // Mutations
  mutations: {
    addTask(state, title: string) {
      state.tasks.push({
        id: Date.now(),
        title,
        completed: false,
      });
    },
    removeTask(state, id: number) {
      state.tasks = state.tasks.filter((task) => task.id !== id);
    },
    toggleTask(state, id: number) {
      const task = state.tasks.find((task) => task.id === id);
      if (task) {
        task.completed = !task.completed;
      }
    },
    setFilter(state, filter: "all" | "completed" | "incomplete") {
      state.filter = filter;
    },
  },

  // Actions
  actions: {
    addTask({ commit }, title: string) {
      commit("addTask", title);
    },
    toggleTask({ commit }, id: number) {
      commit("toggleTask", id);
    },
    setFilter({ commit }, filter: "all" | "completed" | "incomplete") {
      commit("setFilter", filter);
    },
  },
});
// TodoVuex.vue

<template>
  <div class="todo">
    <select
      v-model="store.state.filter"
      @update:modelValue="store.commit('setFilter', $event)"
    >
      <option value="all">All</option>
      <option value="completed">Completed</option>
      <option value="incomplete">Incomplete</option>
    </select>

    <ul>
      <li v-for="task in store.getters.filteredTasks" :key="task.id">
        <input
          type="checkbox"
          v-model="task.completed"
          @update="store.commit('toggleTask', task.id)"
        />

        {{ task.title }}
        <button @click="store.commit('removeTask', task.id)">X</button>
      </li>
    </ul>

    <input
      v-model="newTaskTitle"
      @keyup.enter="addTask"
      placeholder="New task"
    />
    <button @click="addTask">Add Task</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import { useStore } from "vuex";

import { type TasksState, key } from "../store/vuex/tasks";

const store = useStore<TasksState>(key);

const newTaskTitle = ref("");

const addTask = () => {
  if (newTaskTitle.value.trim()) {
    store.commit("addTask", newTaskTitle.value);
    newTaskTitle.value = "";
  }
};
</script>

Top Vue.js Component UI Libraries

Component libraries are a great way to speed up your development when you’re not required to follow any custom design guidelines. Just keep in mind that they come with some limitations, and sometimes, it just makes more sense to build your own components based on features you’ll actually need.

There are many great options out there; below, you’ll find an overview of some of the top Vue.js UI component libraries that are suitable for Vue 3 usage.

  • Vuetify: Designed around Google's Material Design principles, Vuetify provides a comprehensive suite of UI components and tools that ensure consistency and accessibility in design. Its extensive feature set makes it an excellent choice for building large, feature-rich applications that demand a polished and professional look.

  • Element Plus: As a clean and modern library inspired by Element UI, Element Plus stands out for its ease of use and simplicity. This makes it an ideal solution for small to medium-sized applications where developers value intuitive design and straightforward implementation.

  • Quasar: Embracing a cross-platform philosophy, Quasar is a platform-agnostic UI library designed for developing apps that run seamlessly across multiple environments. Its standout point is its ability to cater to cross-platform needs, making it perfect for developers building apps for web, desktop, and mobile.

  • Naive UI: Known for its modern and minimalistic approach, Naive UI is a lightweight library tailored for developers who prioritize simplicity. It excels in small-scale projects or applications where clean and efficient design is paramount without unnecessary complexity.

  • Ant Design Vue: Built for professional, enterprise-level applications, Ant Design Vue emphasizes a polished and business-oriented style. It is highly suited for large-scale business applications, offering components and tools designed to meet the specific needs of enterprise-grade projects.

Scalability and Maintainability with Proven Tools and Common Sense

This section is not strictly about Vue tools but rather the best practices regarding their usage if (when) your project starts growing beyond a couple of components. Let’s see what we can do to stop the chaos before it starts.

Extract Reusable Logic to Composables and Helpers

Let’s say that you created a sorting function and find yourself re-declaring it over and over again across multiple files. I’d say it would make sense to declare it in a separate file in your helpers' folder and just import it into as many files as you need. This way, you can proceed with all stateless logic. 

What about stateful logic? Composables to the help! - Here, you can utilize modern tools for Vue.js, like Vue 3’s Composition API, to the fullest, including refs, watchers, and lifecycle hooks. For example, you need to determine the device type based on the viewport width multiple times throughout your project. Simply create a composable like this one:

// useBreakpoints.ts

import { ref, computed } from 'vue';

export function useBreakpoints() {
  const width = ref(window.innerWidth);

  // Computed properties for each device type
  const isMobile = computed(() => width.value < 768);
  const isTablet = computed(() => width.value >= 768 && width.value < 1024);
  const isDesktop = computed(() => width.value >= 1024);

  // Function to update the current width
  const onResize = () => {
    width.value = window.innerWidth;
  };

  return {
    width,
    isMobile,
    isTablet,
    isDesktop,
    onResize,
  };
}

Then trigger the onResize function in a resize event listener in your root component:

// App.vue

<template>
  <router-view />
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useBreakpoints } from './composables/useBreakpoints';

const { onResize } = useBreakpoints();
  
onMounted(() => {
  window.addEventListener('resize', onResize);
});

// Remember to remove the event listener!
onUnmounted(() => {
  window.removeEventListener('resize', onResize);
});
</script>

Now you can easily access the computed properties in any component:

// Component.vue

<template>
  <div class="component">
    <span v-if="isMobile">I'm a mobile device!</span>
    <span v-if="isTablet">I'm a tablet device!</span>
    <span v-if="isDesktop">I'm a desktop device!</span>
  </div>
</template>

<script setup lang="ts">
import { useBreakpoints } from './composables/useBreakpoints';

const { isMobile, isTablet, isDesktop } = useBreakpoints();
</script>

Use TypeScript

It might sound obvious, but please make sure you do! In the beginning, you may be somewhat overwhelmed, but after a bit of getting used to it, you’ll not start any new project without it. Sure, you will spend some time on the setup and type declarations, but this will undoubtedly save you lots of time and many headaches moving forward.

With TS, you’ll be able to catch many potential bugs as early as in your IDE while also getting the most out of your Intellisense plugin autocompletion, which will surely speed up development.

A Good, Modular Structure Goes a Long Way

Make sure to decide on a specific folder structure for your project early on in the process. Group your components, services and state by feature or domain and stick to it when expanding your app. In this way you will end up with a clean outlook and will always know exactly where things are. And it will surely support your Vue.js performance optimization.

Stick to a (Code) Style Guide - Be Consistent!

Template or script first? Single or double quotes? Semicolons or no semicolons?  You can pretty much approach this exactly as you like (except when you’re working with a team - in that case please make sure you and your colleagues are all on the same page, it’s simply good manners), just make sure you stick with it!

In addition, you can utilize tools like ESLint and Prettier to enforce your rules and even automatically format your code upon write or save. Another thing I personally like to do, and that I feel is essential when working with script setup and Composition API, is following a specific order between my script tags.

My go-to order is:

  1. Vue-specific imports

  2. Third-party composable imports (like useStore, useRoute, Vueuse composables etc.)

  3. Third-party library imports

  4. Custom helpers & utils imports

  5. Component imports

  6. Type imports

  7. Props declaration

  8. Emits declaration

  9. Composable initialization (if possible; if not, then as close to the required ref / computed as possible)

  10. Ref declarations

  11. Computed property declarations

  12. Function declarations

  13. Watchers

  14. Lifecycle hooks

Following the same specific order in every component means that you can quickly find what you’re looking for, no matter which area of your application you're in.

Find a Balance Between Prop Drilling and Global Vue.js State Management

Too much prop drilling is tedious and bad, but the same stands for state management.

I wouldn’t say that there's a clear line to be crossed here. Still, as a rule of thumb, I would stick to prop drilling within the children of one parent component and move the property to a Pinia store as soon as it needs to be accessed from a different parent-children component group.

And for Pete’s sake, do not set up an event bus. It’s just bad practice. Instead, stick to proper event emitting.

Keep it simple (and DRY), stupid! (a rather lax but smart approach to code documentation)

I wanted to simply write “document your code,” but many times in its conventional sense, this would be overkill. There are a couple of things you can do before jumping into your readme file or team confluence to add a new section:

  • Simplify, extract, and reuse your functions

  • Use proper and straightforward names for all your variables and functions - this will make your code mostly self-explanatory

  • If necessary, just add an in-code comment, for example, when you need to write an unexpected line to handle some weird edge-case

Optimize your code as you go by

Import only portions of large libraries that you need instead of the whole thing,

Split and Lazy-load your code,

// in vue-router

const routes = [
  {
    path: '/lazy',
    component: () => import('./components/Lazy.vue'),
  },
];

// in components

import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
);

Avoid unnecessary reactivity where possible; you can, for example, use shallowRef for objects that don’t need deep reactivity,

Minimize DOM updates:

  • Use v-if rather than v-show for infrequently displayed elements,

  • Add a key attribute to in-loop components,

Use Vueuse - why reinvent the wheel (the composable in this case) when you can simply plug into this gem of a library containing over 200 super useful composables for almost any use case you could imagine, created by Anthony Fu (core team member of Vue.js, Nuxt & Vite),

Use the loading="lazy" attribute for images,

Don’t overdo it with dependencies:

  • Make sure that you actually need a library before installing it,

  • Check for unused libraries and remove them on a regular basis.

Seamless Deployment with Render.com

While there certainly are many excellent options for deploying your app, one stands out to me.

Unlike many others, Render is a cloud platform that is not tied to any specific provider or tech stack. It’s very flexible and easy to use, with quite a generous free tier for you to play around with!

Let’s say you want to deploy a static website. The only thing you need is an account on GitHub, GitLab, or Bitbucket, where your code is stored - Render can authenticate and tie itself to your account through any of these.

When you’re inside, simply select New → Static Site. You'll be prompted to select a repository from your selected developer platform. After that, simply select your desired branch, f. ex. “main,” provide the necessary build commands (for your static Vue + Vite app, “yarn; yarn build” will be enough), and your project’s publish directory (“dist” in your case) and you’re good to go! From now on, Render will automatically deploy your static site on every change made to the provided branch. Easy as pie, right?

Build Future-Proof Vue.js Apps with Modern Tools and Best Practices

I know that this post turned out to be quite long, so let’s wrap it up… As probably every other developer, I’m opinionated about what works for me and what doesn’t. I highly recommend that you do a bit of soul-searching on your own to find the perfect set of modern tools for Vue.js. But I’d be thrilled if any of my advice should help you, even in the smallest way. Nonetheless, I firmly believe that one should utilize modern tools where possible instead of clinging to the past, which may sometimes seem easier.

Also, remember that small changes make a huge impact - implementing a few good practices on a daily basis and leveraging the top Vue.js developer tools will greatly improve the quality of your project.

For those that just scrolled through the text - TL;DR: Use Vite, Pinia and TypeScript while keeping your project structure and code clean - this should be a good starting point for any new Vue 3 project. Thank you all for reading and happy coding!

Antek Karlsson
Antek Karlsson
Frontend Developer at Monterail
Antoni is an open-minded frontend developer specializing in Vue.js and TypeScript. With a background in logistics, manufacturing, and sales, he combines technical expertise with valuable business insight in his projects.