codelessgenie guide

Vue 3: New Features and How to Take Advantage of Them

Vue 3 is a complete rewrite of Vue.js, focusing on **performance**, **modularity**, and **TypeScript integration**. Key goals included reducing bundle size, improving reactivity, and enabling better code organization for large-scale applications. Unlike Vue 2, which used a monolithic architecture, Vue 3 is designed with tree-shaking in mind, allowing unused features to be excluded from production builds. Whether you’re building a small single-page app (SPA) or a complex enterprise application, Vue 3’s features empower you to write more expressive, maintainable, and efficient code. Let’s dive into the most impactful additions.

Since its release in September 2020, Vue 3 has revolutionized the way developers build web applications with its enhanced performance, improved developer experience, and a host of new features. Built from the ground up with TypeScript, Vue 3 addresses longstanding limitations of Vue 2 while introducing powerful tools like the Composition API, a reworked reactivity system, and optimized rendering. Whether you’re migrating from Vue 2 or starting fresh, understanding these features will help you write cleaner, more maintainable, and performant code.

Table of Contents

  1. Introduction to Vue 3
  2. The Composition API: Flexible Code Organization
  3. Revamped Reactivity System: Proxy-Based Observation
  4. Template Syntax Improvements
  5. First-Class TypeScript Support
  6. Teleport: Render Content Anywhere
  7. Suspense: Handling Async Operations Gracefully
  8. Global API Changes: createApp and Beyond
  9. Performance Boosts: Smaller, Faster, Smarter
  10. Migration Tips from Vue 2
  11. Conclusion
  12. References

The Composition API: Flexible Code Organization

One of Vue 3’s most significant additions is the Composition API, an alternative to Vue 2’s Options API. While the Options API groups code by options (e.g., data, methods, computed), the Composition API lets you group code by feature, making it easier to reuse logic and manage complex components.

Why It Matters:

  • Better Code Organization: Logic for a single feature (e.g., form validation, data fetching) lives in one place, rather than being spread across data, methods, and watch.
  • Reusability: Extract logic into composable functions that can be shared across components.
  • Type Safety: Works seamlessly with TypeScript for better autocompletion and error checking.

How to Use It:

The Composition API is accessed via the setup() function (or the syntactic sugar script setup). Here’s a quick example:

Example: Counter with Composition API

<!-- Using <script setup> (recommended for simplicity) -->
<script setup>
import { ref, computed } from 'vue';

// Reactive state
const count = ref(0);

// Computed property
const doubleCount = computed(() => count.value * 2);

// Method
const increment = () => {
  count.value++;
};
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

Key Composition API Functions:

  • ref: Creates a reactive reference for primitive values (e.g., numbers, strings). Access/modify via .value in scripts.
  • reactive: Creates a reactive object for non-primitive values (e.g., objects, arrays).
  • computed: Defines a reactive computed property.
  • watch/watchEffect: Watch for changes in reactive state and run side effects.
  • onMounted, onUnmounted, etc.: Lifecycle hooks (replace mounted, unmounted in Options API).

Composables: Reusing Logic

A major benefit of the Composition API is the ability to create composables—reusable functions that encapsulate logic. For example, a useFetch composable for data fetching:

// composables/useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);

  onMounted(async () => {
    try {
      const response = await fetch(url);
      data.value = await response.json();
    } catch (err) {
      error.value = err.message;
    }
  });

  return { data, error };
}

Use it in a component:

<script setup>
import { useFetch } from './composables/useFetch';

const { data, error } = useFetch('https://api.example.com/data');
</script>

<template>
  <div v-if="error">Error: {{ error }}</div>
  <div v-else-if="data">Data: {{ data }}</div>
  <div v-else>Loading...</div>
</template>

Revamped Reactivity System: Proxy-Based Observation

Vue 3’s reactivity system is rewritten using ES6 Proxies, replacing Vue 2’s Object.defineProperty. This fixes longstanding limitations of Vue 2’s reactivity and makes it more powerful.

Vue 2 Limitations Addressed:

  • Dynamic Property Addition: In Vue 2, adding a new property to a reactive object required Vue.set(obj, 'key', value); with Proxies, this works out of the box.
  • Array Index Mutations: Vue 2 couldn’t detect changes like arr[0] = newValue; Vue 3 handles this natively.
  • Better Performance: Proxies are more efficient than Object.defineProperty, especially for large objects.

How to Use Reactive State:

Vue 3 provides two main ways to create reactive state: ref (for primitives) and reactive (for objects/arrays).

Example: Reactive Object

<script setup>
import { reactive } from 'vue';

const user = reactive({
  name: 'Alice',
  age: 30
});

// Modify properties directly (no need for Vue.set!)
user.age = 31;
user.email = '[email protected]'; // New property added reactively
</script>

<template>
  <div>
    <p>Name: {{ user.name }}</p>
    <p>Age: {{ user.age }}</p>
    <p>Email: {{ user.email }}</p> <!-- Works! -->
  </div>
</template>

Example: Reactive Array

<script setup>
import { reactive } from 'vue';

const todos = reactive([
  { id: 1, text: 'Learn Vue 3' }
]);

// Mutate array directly (no need for Vue.set or splice!)
todos.push({ id: 2, text: 'Build an app' });
todos[0].text = 'Master Vue 3'; // Nested changes are reactive
</script>

Template Syntax Improvements

Vue 3 introduces several template enhancements to make templates more expressive and flexible.

1. Multi-Root Components (Fragments)

Vue 2 required components to have a single root element. Vue 3 lifts this restriction, allowing multi-root components (fragments).

Example: Multi-Root Component

<template>
  <header>Header</header>
  <main>Main Content</main>
  <footer>Footer</footer>
</template>

This avoids unnecessary wrapper divs and simplifies component composition.

2. Improved v-model

Vue 3 overhauls v-model to be more flexible. Key changes:

  • Customizable Prop/Emit Names: By default, v-model uses modelValue as the prop and update:modelValue as the event, but you can customize these.
  • Multiple v-model Bindings: A component can support multiple v-model bindings using arguments.

Example: Custom Input with v-model

<!-- ChildComponent.vue -->
<script setup>
defineProps(['searchQuery']);
const emit = defineEmits(['update:searchQuery']);
</script>

<template>
  <input
    :value="searchQuery"
    @input="emit('update:searchQuery', $event.target.value)"
  />
</template>

Use in parent with a custom argument:

<template>
  <ChildComponent v-model:searchQuery="query" />
  <!-- Equivalent to: <ChildComponent :searchQuery="query" @update:searchQuery="query = $event" /> -->
</template>

Example: Multiple v-model Bindings

<template>
  <UserForm 
    v-model:firstName="firstName" 
    v-model:lastName="lastName" 
  />
</template>

3. v-if/v-for Precedence

In Vue 2, v-for took precedence over v-if on the same element, leading to confusion. Vue 3 reverses this: v-if now has higher precedence, making the behavior more intuitive.

First-Class TypeScript Support

Vue 3 is written in TypeScript, enabling native type support throughout the framework. This means better autocompletion, type checking, and documentation in IDEs like VS Code.

How to Use TypeScript in Vue 3:

1. script setup with TypeScript

Add lang="ts" to the script tag and use TypeScript types directly:

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

// Typed ref
const count: number = ref(0).value; // Explicit type (optional, since ref infers type)

// Interface for props
interface User {
  name: string;
  age: number;
}

const user: User = { name: 'Bob', age: 25 };
</script>

2. Typed Props and Emits

Use defineProps and defineEmits with TypeScript to enforce types:

<script setup lang="ts">
// Typed props
const props = defineProps<{
  message: string;
  count?: number; // Optional prop
}>();

// Typed emits
const emit = defineEmits<{
  (e: 'increment', value: number): void;
  (e: 'reset'): void;
}>();

const handleClick = () => {
  emit('increment', 1);
};
</script>

Teleport: Render Content Anywhere

The <teleport> component lets you render a part of a component’s template in a different location in the DOM tree, outside the component’s own DOM hierarchy. This is useful for modals, tooltips, or notifications that need to escape parent CSS constraints (e.g., overflow: hidden).

Example: Teleporting a Modal

<template>
  <button @click="showModal = true">Open Modal</button>

  <teleport to="body"> <!-- Render modal in <body> -->
    <div v-if="showModal" class="modal">
      <h2>Modal Title</h2>
      <p>This modal is rendered in <body>!</p>
      <button @click="showModal = false">Close</button>
    </div>
  </teleport>
</template>

<script setup>
import { ref } from 'vue';
const showModal = ref(false);
</script>

<style>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 20px;
  background: white;
  border: 1px solid #ccc;
}
</style>

By teleporting the modal to <body>, it avoids being clipped by parent containers with overflow: hidden.

Suspense: Handling Async Operations Gracefully

Suspense is a built-in component for handling asynchronous operations, such as loading async components or fetching data. It allows you to define a fallback UI (e.g., a loading spinner) while waiting for async content to resolve.

Example: Async Component with Suspense

<!-- ParentComponent.vue -->
<script setup>
import { defineAsyncComponent } from 'vue';

// Async component (loaded dynamically)
const AsyncComponent = defineAsyncComponent(() => 
  import('./AsyncComponent.vue')
);
</script>

<template>
  <Suspense>
    <!-- Async content to load -->
    <AsyncComponent />

    <!-- Fallback UI shown while loading -->
    <template #fallback>
      <p>Loading...</p>
    </template>
  </Suspense>
</template>
<!-- AsyncComponent.vue -->
<script setup>
// Simulate async data fetching
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data loaded!'), 2000);
});

const data = await fetchData(); // Top-level await in script setup
</script>

<template>
  <div>{{ data }}</div>
</template>

Suspense waits for AsyncComponent to load and resolve its async data before rendering it, showing “Loading…” in the meantime.

Global API Changes: createApp and Beyond

Vue 3 replaces Vue 2’s global Vue constructor with a more modular createApp API. This avoids polluting the global namespace and makes apps more isolated and testable.

Key Changes:

  • createApp Instead of new Vue: Creates an app instance with its own scope.
  • Instance Methods for Plugins/Components: Use app.use(plugin), app.component(name, component), etc., instead of global Vue.use or Vue.component.

Example: Creating an App

// Vue 2
import Vue from 'vue';
import App from './App.vue';

new Vue({
  render: h => h(App)
}).$mount('#app');
// Vue 3
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import MyComponent from './components/MyComponent.vue';

const app = createApp(App);

// Register router plugin
app.use(router);

// Register component globally (scoped to this app)
app.component('MyComponent', MyComponent);

// Mount the app
app.mount('#app');

This approach ensures plugins and components are scoped to the app instance, preventing conflicts in multi-app scenarios (e.g., micro-frontends).

Performance Boosts: Smaller, Faster, Smarter

Vue 3 delivers significant performance improvements over Vue 2:

1. Smaller Bundle Size

Vue 3’s core runtime is ~10KB gzipped (compared to Vue 2’s ~33KB), thanks to tree-shaking. Unused features (e.g., Transition, KeepAlive) are excluded from production builds.

2. Faster Virtual DOM

Vue 3’s virtual DOM is optimized with:

  • Compiler-Informed Fast Paths: The template compiler generates code that avoids unnecessary diffing.
  • Block Tree Optimization: Groups static nodes to reduce re-render overhead.

3. Improved Rendering Performance

Benchmarks show Vue 3 renders up to 55% faster than Vue 2 and updates up to 133% faster in certain scenarios (source: Vue 3 RFC).

Migration Tips from Vue 2

If you’re migrating from Vue 2, here are key steps to ensure a smooth transition:

  1. Use the Migration Build: Vue provides a migration build that lets you run Vue 2 and 3 code side-by-side, with warnings for deprecated features.
  2. Replace Filters with Computed Properties: Vue 3 removes filters; use computed properties or methods instead.
  3. Update v-bind.sync to v-model Arguments: Vue 2’s .sync modifier is replaced with v-model:propName.
  4. Refactor Options API to Composition API (Optional): While the Options API is still supported, the Composition API offers better scalability for large apps.

Conclusion

Vue 3 is a leap forward for the framework, offering improved reactivity, better code organization with the Composition API, native TypeScript support, and performance optimizations. Whether you’re building a small app or a large enterprise system, these features empower you to write cleaner, more maintainable, and efficient code.

By adopting Vue 3, you’ll benefit from a modern, modular architecture that scales with your project’s needs. Start experimenting today—your future self (and your users) will thank you!

References