Core Patterns in Vue 3: From Setup to Advanced Communication

Creating a Vue 3 SPA

Two recommended approaches exist for scaffolding a Vue 3 project:

  • Vite – minimal, fast, exclusively for Vue 3.x
  • Vue CLI – feature-rich, supports both Vue 2.x and Vue 3.x

Template Root Elements

Unlike Vue 2.x, Vue 3.x allows multiple root nodes inside a <template>. A wrapper <div> is no longer mandatory.

Dynamic Class Binding

Ternary expression for a single class

<h3 class='base' :class="isActive ? 'active' : ''">Content</h3>
<button @click='isActive = !isActive'>Toggle Active</button>
data() {
  return { isActive: true }
}

Array syntax for multiple classes

<h3 class='base' :class="[isActive ? 'active' : '', isDeleted ? 'deleted' : '']">Content</h3>
data() {
  return {
    isActive: true,
    isDeleted: false
  }
}

Object syntax for multiple classes

<h3 class='base' :class='classStates'>Content</h3>
<button @click='classStates.active = !classStates.active'>Toggle Active</button>
<button @click='classStates.deleted = !classStates.deleted'>Toggle Deleted</button>
data() {
  return {
    classStates: {
      active: true,
      deleted: false
    }
  }
}

Inline Style Binding

Use an object to bind computed style values:

<template>
  <div :style="{ backgroundColor: bg, fontSize: size + 'px' }">{{ label }}</div>
</template>

<script>
export default {
  props: {
    label: { type: String, default: 'Title' },
    bg: { type: String, default: '#f0f0f0' },
    size: { type: Number, default: 16 }
  }
}
</script>

Prop Validation

Basic type checks

props: {
  title: String,
  count: Number,
  isVisible: Boolean,
  items: Array,
  config: Object,
  expiry: Date,
  handler: Function,
  symbol: Symbol
}

Custom validator

props: {
  status: {
    validator(value) {
      // Must be one of the allowed states
      return ['pending', 'approved', 'rejected'].includes(value)
    }
  }
}

Custom Events and emits

Vue 3 requires explicit declaration of emitted events.

<template>
  <button @click='increment'>+1</button>
</template>

<script>
export default {
  emits: ['counter-update'],
  methods: {
    increment() {
      this.$emit('counter-update', this.currentCount)
    }
  }
}
</script>

Parent‑Child Two‑Way Data Sync

Using v-model with a custom prop:

<!-- Parent.vue -->
<ChildComponent v-model:value='parentValue'></ChildComponent>

<!-- Child.vue -->
<template>
  <button @click='change'>Modify</button>
</template>

<script>
export default {
  props: ['value'],
  emits: ['update:value'],
  methods: {
    change() {
      this.$emit('update:value', this.value + 10)
    }
  }
}
</script>

Sibling Communication with mitt

Vue 3 no longer has a built‑in EventBus. Use the lightweight mitt library instead.

npm install mitt
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();

Providing Data to Descendant Components

Non‑reactive data

// Ancestor.vue
export default {
  data() {
    return { themeColor: 'blue' }
  },
  provide() {
    return {
      sharedColor: this.themeColor
    }
  }
}
<!-- Descendant.vue -->
<template>
  <h5>{{ sharedColor }}</h5>
</template>

<script>
export default {
  inject: ['sharedColor']
}
</script>

Responsive data

Wrap the value with computed so that changes are reflected in all inejctors.

// Ancestor.vue
import { computed } from 'vue';

export default {
  data() {
    return { bgColor: 'white' }
  },
  provide() {
    return {
      responsiveBg: computed(() => this.bgColor)
    }
  }
}
<!-- Descendant.vue -->
<template>
  <div :style="{ background: responsiveBg.value }">Content</div>
</template>

<script>
export default {
  inject: ['responsiveBg']
}
</script>

Global Axios Configuration

Vue 2

// main.js
import Vue from 'vue';
import axios from 'axios';

axios.defaults.baseURL = 'https://api.example.com';
Vue.prototype.$http = axios;

Vue 3

// main.js
import { createApp } from 'vue';
import axios from 'axios';
import App from './App.vue';

const app = createApp(App);
axios.defaults.baseURL = 'https://api.example.com';
app.config.globalProperties.$http = axios;
app.mount('#app');

Usage inside a component:

methods: {
  async fetchData() {
    const { data } = await this.$http.get('/items', {
      params: { page: 1 }
    });
    console.log(data);
  }
}

Vue Router 4 Setup

import { createRouter, createWebHashHistory } from 'vue-router';
import HomeView from './Home.vue';
import MovieView from './Movie.vue';

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/home', component: HomeView },
    { path: '/movie', component: MovieView }
  ]
});

export default router;

Axios Interceptors

Request interceptor for authentciation

axios.interceptors.request.use(config => {
  config.headers['Authorization'] = 'Bearer ' + getToken();
  return config;
});

Loading indicator with Element UI

import { Loading } from 'element-ui';
let loadingInstance = null;

axios.interceptors.request.use(config => {
  loadingInstance = Loading.service({ fullscreen: true });
  return config;
});

axios.interceptors.response.use(
  response => {
    loadingInstance.close();
    return response;
  },
  error => {
    loadingInstance.close();
    return Promise.reject(error);
  }
);

Proxy for Cross‑Origin Requests

During development, devServer.proxy can forward API requests:

// vue.config.js or vite.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://backend.example.com',
        changeOrigin: true
      }
    }
  }
};

This is a development‑only convenience; in production the API server must enable CORS.

Tags: Vue.js vue3 frontend Tutorial javascript

Posted on Thu, 01 Oct 2026 16:54:38 +0000 by emehrkay