🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Vue 3 Complete Guide 2026: Composition API, Pinia and Nuxt.js

⏱️4 min read  ·  858 words

Vue 3 with the Composition API, Pinia state management, and Nuxt.js for full-stack — Vue is the second most popular JavaScript framework in 2026 with an excellent developer experience. This complete guide covers Vue 3’s Composition API, reactivity system, and production patterns.

Setup

# Create new project
npm create vue@latest my-app

# Choose: TypeScript, Vue Router, Pinia, ESLint, Prettier
cd my-app && npm install && npm run dev

Composition API

<!-- Counter.vue — script setup syntax (preferred) -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';

// ref — reactive primitive
const count = ref(0);
const name = ref('World');

// computed — derived state (cached)
const doubled = computed(() => count.value * 2);
const greeting = computed(() => `Hello, ${name.value}! Count: ${count.value}`);

// watch — side effects
watch(count, (newVal, oldVal) => {
  console.log(`Count changed: ${oldVal} → ${newVal}`);
  document.title = `Count: ${newVal}`;
});

// watchEffect — auto-tracks dependencies
watchEffect(() => {
  localStorage.setItem('count', count.value.toString());
});

// Lifecycle hooks
onMounted(() => {
  console.log('Component mounted!');
  count.value = parseInt(localStorage.getItem('count') || '0');
});

function increment() {
  count.value++;
}

function reset() {
  count.value = 0;
}
</script>

<template>
  <div class="counter">
    <h2>{{ greeting }}</h2>
    <p>Doubled: {{ doubled }}</p>
    <!-- v-model works with refs automatically -->
    <input v-model="name" placeholder="Your name" />
    <div>
      <button @click="increment">+1</button>
      <button @click="reset">Reset</button>
    </div>
  </div>
</template>

Reactive Objects with reactive()

<script setup lang="ts">
import { reactive, toRefs } from 'vue';

// reactive — for objects (no .value needed)
const state = reactive({
  count: 0,
  name: 'Alice',
  todos: [] as string[],
});

// Destructure with toRefs (maintains reactivity)
const { count, name } = toRefs(state);

// Mutate directly
function addTodo(text: string) {
  state.todos.push(text);
}
</script>

Components and Props

<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
  user: {
    id: number;
    name: string;
    email: string;
    avatarUrl?: string;
  };
  isSelected?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  isSelected: false,
});

const emit = defineEmits<{
  select: [userId: number];
  delete: [userId: number];
}>();

function handleSelect() {
  emit('select', props.user.id);
}
</script>

<template>
  <div
    class="user-card"
    :class="{ 'user-card--selected': isSelected }"
    @click="handleSelect"
  >
    <img
      v-if="user.avatarUrl"
      :src="user.avatarUrl"
      :alt="user.name"
    />
    <div>
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
    <button @click.stop="emit('delete', user.id)">Delete</button>
  </div>
</template>

Composables — Custom Hooks

// composables/useUsers.ts
import { ref, computed } from 'vue';

interface User { id: number; name: string; email: string; }

export function useUsers() {
  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);
  const searchQuery = ref('');

  const filteredUsers = computed(() =>
    users.value.filter(u =>
      u.name.toLowerCase().includes(searchQuery.value.toLowerCase())
    )
  );

  async function fetchUsers() {
    loading.value = true;
    error.value = null;
    try {
      const response = await fetch('/api/users');
      users.value = await response.json();
    } catch (e) {
      error.value = 'Failed to load users';
    } finally {
      loading.value = false;
    }
  }

  async function createUser(data: Omit<User, 'id'>) {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(data),
      headers: { 'Content-Type': 'application/json' },
    });
    const newUser = await response.json();
    users.value = [...users.value, newUser];
  }

  return { users, loading, error, searchQuery, filteredUsers, fetchUsers, createUser };
}

// Usage in component
const { users, loading, error, searchQuery, filteredUsers, fetchUsers } = useUsers();
onMounted(fetchUsers);

Pinia State Management

// stores/userStore.ts
import { defineStore } from 'pinia';

export const useUserStore = defineStore('users', {
  state: () => ({
    users: [] as User[],
    currentUser: null as User | null,
    loading: false,
  }),

  getters: {
    activeUsers: (state) => state.users.filter(u => u.active),
    userCount: (state) => state.users.length,
  },

  actions: {
    async fetchUsers() {
      this.loading = true;
      try {
        this.users = await api.getUsers();
      } finally {
        this.loading = false;
      }
    },

    async createUser(data: CreateUserInput) {
      const user = await api.createUser(data);
      this.users.push(user);
      return user;
    },

    logout() {
      this.currentUser = null;
    }
  }
});

// In component
import { useUserStore } from '@/stores/userStore';
const store = useUserStore();
await store.fetchUsers();
console.log(store.userCount);
store.logout();

Vue vs React: When to Choose Vue

  • Choose Vue when: Simpler learning curve needed, template-first approach preferred, smaller team, gradual migration from jQuery/Vanilla JS
  • Choose React when: Large team, complex state management needed, Next.js ecosystem, React Native for mobile
  • Both are excellent for most web apps — the choice often comes down to team familiarity

Vue 3 in 2026 with script setup, Composition API, and Pinia is a mature, developer-friendly framework. The template syntax is more approachable than JSX for many developers, and the Composition API gives full power for complex components. For full-stack, pair with Nuxt.js for the same productivity gains as Next.js for React.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা