SB

Getting Started with Vue 3: A Beginner’s Guide

Getting Started with Vue 3: A Beginner’s Guide

Introduction

Vue 3 represents a significant evolution in JavaScript frameworks, built from the ground up with performance and developer experience in mind. What sets Vue apart isn’t just its gentle learning curve, but how it scales from simple interactive widgets to complex single-page applications without forcing you to rewrite everything.

This guide walks through Vue 3’s core concepts and practical implementation, giving you the foundation to build real applications. We’ll cover the essential patterns you’ll use daily and the gotchas that trip up most developers starting out.

Why Choose Vue 3?

Vue 3 isn’t just an incremental update. The rewrite introduced fundamental improvements that address real-world development pain points:

Composition API: The biggest game-changer. Instead of scattering related logic across data, methods, and computed, you can group functionality together. This becomes crucial as components grow beyond simple examples.

Better TypeScript Support: Vue 3 was written in TypeScript, so the type inference actually works. No more fighting with complex type definitions just to get autocomplete.

Performance Gains: The new reactivity system is faster and uses less memory. Tree-shaking means your bundles only include what you actually use.

Fragment Support: You can finally return multiple root elements without wrapping them in a useless <div>.

💡 Tip: Don’t feel pressured to use the Composition API immediately. The Options API still works and is perfectly fine for most use cases. Composition API shines when you need to share logic between components or handle complex state.

Setting Up Vue 3

Prerequisites

You’ll need Node.js 16+ for the best experience. While Vue works with older versions, you’ll miss out on some tooling improvements.

Installation Options

Vite is the Way Forward

Skip Vue CLI for new projects. Vite is faster, simpler, and officially recommended:

npm create vue@latest my-project
cd my-project
npm install
npm run dev

The Vue create command gives you options for TypeScript, router, testing, and linting. Start minimal and add what you need.

Quick Prototyping with CDN

For experimenting or adding Vue to existing pages:

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
  Vue.createApp({
    data() {
      return { message: 'Hello Vue!' }
    }
  }).mount('#app')
</script>

⚠️ Warning: CDN builds include the full compiler and are larger. Use the runtime-only build for production.

Creating Your First Vue 3 Component

Understanding Single File Components

Vue’s Single File Component (SFC) format keeps related code together while maintaining clear separation:

<template>
  <div class="greeting">
    <h1>{{ displayMessage }}</h1>
    <button @click="toggleMessage" :disabled="isLoading">
      {{ buttonText }}
    </button>
  </div>
</template>

<script>
export default {
  name: 'GreetingCard',
  data() {
    return {
      message: "Welcome to Vue 3!",
      isToggled: false,
      isLoading: false
    };
  },
  computed: {
    displayMessage() {
      return this.isToggled ? "Thanks for clicking!" : this.message;
    },
    buttonText() {
      return this.isLoading ? "Loading..." : "Click Me";
    }
  },
  methods: {
    async toggleMessage() {
      this.isLoading = true;
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 500));
      this.isToggled = !this.isToggled;
      this.isLoading = false;
    }
  }
};
</script>

<style scoped>
.greeting {
  text-align: center;
  padding: 2rem;
}

h1 {
  color: #42b983;
  margin-bottom: 1rem;
}

button {
  background-color: #42b983;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: opacity 0.2s;
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

button:hover:not(:disabled) {
  background-color: #369870;
}
</style>

Key Concepts Explained

Reactivity: When isToggled changes, displayMessage automatically recalculates and the DOM updates. This happens because Vue tracks dependencies during the initial render.

Computed vs Methods: Computed properties cache their results and only recalculate when dependencies change. Methods run every time they’re called.

Event Handling: @click is shorthand for v-on:click. Vue automatically passes the event object if your method needs it.

📌 Note: The name property isn’t required but helps with debugging in Vue DevTools.

The Composition API

The Composition API solves the problem of logic scattered across different sections in larger components. Here’s the same component using Composition API:

<template>
  <div class="greeting">
    <h1>{{ displayMessage }}</h1>
    <button @click="toggleMessage" :disabled="isLoading">
      {{ buttonText }}
    </button>
  </div>
</template>

<script>
import { ref, computed } from 'vue';

export default {
  name: 'GreetingCard',
  setup() {
    // Reactive state
    const message = ref("Welcome to Vue 3!");
    const isToggled = ref(false);
    const isLoading = ref(false);

    // Computed properties
    const displayMessage = computed(() => 
      isToggled.value ? "Thanks for clicking!" : message.value
    );
    
    const buttonText = computed(() => 
      isLoading.value ? "Loading..." : "Click Me"
    );

    // Methods
    const toggleMessage = async () => {
      isLoading.value = true;
      await new Promise(resolve => setTimeout(resolve, 500));
      isToggled.value = !isToggled.value;
      isLoading.value = false;
    };

    return {
      displayMessage,
      buttonText,
      toggleMessage,
      isLoading
    };
  }
};
</script>

Composition API Benefits

Logical Grouping: Related state and functions stay together, making complex components easier to understand.

Reusability: Extract common logic into composables (Vue’s version of React hooks):

// composables/useToggle.js
import { ref } from 'vue';

export function useToggle(initialValue = false) {
  const isToggled = ref(initialValue);
  const toggle = () => isToggled.value = !isToggled.value;
  
  return { isToggled, toggle };
}

Better TypeScript: Type inference works naturally without complex generic gymnastics.

⚠️ Warning: Don’t destructure reactive objects directly. Use toRefs() if you need to destructure refs from a reactive object.

Adding Vue Router

Vue Router handles client-side navigation and is essential for any multi-page application. Here’s a practical setup:

Installation and Basic Setup

npm install vue-router@4
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
import NotFound from '@/views/NotFound.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'), // Lazy loading
    props: true // Pass route params as props
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition;
    return { top: 0 };
  }
});

export default router;
<template>
  <nav>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-link :to="{ name: 'User', params: { id: 123 } }">
      User 123
    </router-link>
  </nav>
  <router-view />
</template>

Programmatic Navigation:

// Inside component
this.$router.push('/about');
this.$router.push({ name: 'User', params: { id: 456 } });

// With Composition API
import { useRouter } from 'vue-router';

export default {
  setup() {
    const router = useRouter();
    
    const goToUser = (id) => {
      router.push({ name: 'User', params: { id } });
    };
    
    return { goToUser };
  }
};

💡 Tip: Use named routes instead of path strings. They’re less error-prone and easier to refactor.

Route Guards and Meta Fields

const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  }
];

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login');
  } else {
    next();
  }
});

Component Communication

Props and Events

Props flow down, events flow up. This unidirectional data flow prevents bugs:

<!-- Parent.vue -->
<template>
  <UserCard 
    :user="selectedUser"
    :loading="isLoading"
    @update="handleUserUpdate"
    @delete="handleUserDelete"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedUser: { name: 'John', email: 'john@example.com' },
      isLoading: false
    };
  },
  methods: {
    handleUserUpdate(updatedUser) {
      this.selectedUser = { ...this.selectedUser, ...updatedUser };
    },
    async handleUserDelete() {
      this.isLoading = true;
      await this.deleteUser(this.selectedUser.id);
      this.isLoading = false;
    }
  }
};
</script>
<!-- UserCard.vue -->
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <button @click="editUser" :disabled="loading">Edit</button>
    <button @click="deleteUser" :disabled="loading">Delete</button>
  </div>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  emits: ['update', 'delete'],
  methods: {
    editUser() {
      this.$emit('update', { name: 'Updated Name' });
    },
    deleteUser() {
      this.$emit('delete');
    }
  }
};
</script>

⚠️ Warning: Always define your emits. Vue 3 will warn about undefinedemits in development, and it helps with debugging.

Styling with Vue

Scoped Styles and CSS Modules

Scoped styles prevent style leakage:

<style scoped>
.button {
  background: blue;
}
/* Compiles to .button[data-v-f3f3eg9] */
</style>

CSS Modules for programmatic access:

<template>
  <button :class="$style.primaryButton">Click me</button>
</template>

<style module>
.primaryButton {
  background: blue;
  color: white;
}
</style>

Dynamic Styling

<template>
  <div :class="{ active: isActive, 'text-danger': hasError }">
    <p :style="{ color: textColor, fontSize: fontSize + 'px' }">
      Dynamic content
    </p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isActive: true,
      hasError: false,
      textColor: '#333',
      fontSize: 16
    };
  }
};
</script>

💡 Tip: For multiple classes, use an array: :class="[baseClass, { active: isActive }]"

Building and Deployment

Optimizing for Production

npm run build

The build process:

  • Tree-shaking: Removes unused code
  • Minification: Reduces file sizes
  • Asset optimization: Optimizes images and other assets
  • Code splitting: Creates separate chunks for better caching

Environment Variables

# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My Vue App
// In your app
const apiUrl = import.meta.env.VITE_API_URL;

📌 Note: Only variables prefixed with VITE_ are exposed to your app code.

Deployment Strategies

Static Hosting (Netlify, Vercel):

  • Build outputs static files
  • Great for most Vue apps
  • Easy CI/CD integration

Server-Side Rendering (Nuxt.js):

  • Better SEO and initial load times
  • More complex deployment
  • Consider for content-heavy sites

Performance Considerations

<!-- Lazy load heavy components -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';

export default {
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./HeavyComponent.vue'))
  }
};
</script>

Common Pitfalls and Best Practices

State Management

  • Use props and events for parent-child communication
  • Consider Pinia for complex shared state
  • Don’t overuse global state; most apps need less than you think

Component Design

  • Keep components focused on a single responsibility
  • Prefer composition over deep nesting
  • Use slots for flexible component APIs

Performance

  • Use v-memo for expensive list items that rarely change
  • Avoid inline objects and functions in templates
  • Consider shallowRef for large objects you don’t need to track deeply

⚠️ Warning: Mutating props directly is an anti-pattern. Always emit events to update parent state.

Conclusion

Vue 3 strikes a balance between simplicity and power that makes it enjoyable to work with. The framework gives you escape hatches when you need them while keeping simple things simple.

Start with the Options API if you’re coming from other frameworks, then gradually adopt Composition API patterns as your applications grow. Focus on understanding reactivity and component communication patterns early, as these concepts underpin everything else.

The Vue ecosystem provides excellent tooling and libraries that handle common needs without forcing architectural decisions on you. This flexibility makes Vue particularly good for teams with varying experience levels and projects with evolving requirements.

About Author

Maulik Paghdal

I'm Maulik Paghdal, the founder of Script Binary and a passionate full-stack web developer. I have a strong foundation in both frontend and backend development, specializing in building dynamic, responsive web applications using Laravel, Vue.js, and React.js. With expertise in Tailwind CSS and Bootstrap, I focus on creating clean, efficient, and scalable solutions that enhance user experiences and optimize performance.

Topics