Curated questions covering Composition API, reactivity, Pinia, Vue Router, directives, lifecycle hooks, Vuex, and Vue 3 features.
Vue.js is a progressive JavaScript framework for building user interfaces. Key features: reactive data binding, component-based architecture, Composition API (Vue 3), Options API, virtual DOM, Vue Router, Pinia state management, single-file components (.vue), and progressive adoption (can be used for a single widget or a full SPA).
// Options API
export default {
data() { return { count: 0 }; },
methods: { increment() { this.count++; } }
};
// Composition API
const count = ref(0);
const increment = () => count.value++;
setup() is the entry point for the Composition API. It runs before the component is created, before beforeCreate. It receives props and context as arguments. Variables and functions returned from setup() are available in the template.
export default {
props: ["userId"],
setup(props, { emit, attrs, slots }) {
const user = ref(null);
onMounted(() => fetchUser(props.userId).then(u => user.value = u));
return { user };
}
};
const count = ref(0);
count.value++; // in JS
// {{ count }} in template (no .value)
const state = reactive({ name: "Alice", age: 30 });
state.name = "Bob"; // direct mutation OK
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
watch(userId, async (newId) => {
user.value = await fetchUser(newId);
});
Composables are functions that encapsulate and reuse stateful logic using the Composition API. They are the Vue 3 equivalent of React hooks. By convention, composable names start with "use".
// useFetch.js
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
fetch(url)
.then(r => r.json())
.then(d => { data.value = d; loading.value = false; })
.catch(e => { error.value = e; loading.value = false; });
return { data, error, loading };
}
// Usage
const { data, loading } = useFetch("/api/users");
<script setup> is syntactic sugar for the Composition API. Variables, functions, and imports declared inside are automatically available in the template. No need to return anything. It is the recommended way to write Vue 3 components.
<script setup>
import { ref, computed } from "vue";
import UserCard from "./UserCard.vue";
const props = defineProps({ userId: Number });
const emit = defineEmits(["update"]);
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
});
const emit = defineEmits(["update:count", "close"]);
emit("update:count", props.count + 1);
</script>
v-model creates two-way data binding. In Vue 3, v-model on a component is shorthand for :modelValue + @update:modelValue. Multiple v-model bindings are supported on a single component.
<!-- Parent -->
<UserInput v-model="username" />
<!-- Equivalent to: -->
<UserInput :modelValue="username" @update:modelValue="username = $event" />
<!-- Child component -->
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
emit("update:modelValue", newValue);
Without key, Vue reuses DOM elements in place when the list changes, which can cause bugs with stateful elements (inputs, animations). With a unique key, Vue can track each element and correctly add/remove/reorder them.
<!-- Bad: index as key causes issues on reorder -->
<li v-for="(item, i) in items" :key="i">{{ item.name }}</li>
<!-- Good: stable unique ID -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
window.addEventListener("resize", handler);
});
onUnmounted(() => {
window.removeEventListener("resize", handler);
});
Pinia is the official Vue state management library (replaces Vuex). Differences: no mutations (only state + actions), better TypeScript support, simpler API, modular by default, devtools support, and works with both Options and Composition API.
// stores/user.js
export const useUserStore = defineStore("user", () => {
const user = ref(null);
const isLoggedIn = computed(() => !!user.value);
async function login(credentials) {
user.value = await api.login(credentials);
}
return { user, isLoggedIn, login };
});
Vue Router is the official routing library for Vue.js. Configure routes with createRouter() and createWebHistory().
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/users/:id", component: User },
{ path: "/admin", component: Admin, meta: { requiresAuth: true } },
{ path: "/:pathMatch(.*)*", component: NotFound }
]
});
Navigation guards control route access. Types: global (beforeEach, afterEach), per-route (beforeEnter), and in-component (onBeforeRouteLeave, onBeforeRouteUpdate).
router.beforeEach((to, from) => {
const auth = useAuthStore();
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { path: "/login", query: { redirect: to.fullPath } };
}
});
const route = useRoute();
const router = useRouter();
const userId = route.params.id;
router.push({ name: "user", params: { id: 1 } });
Teleport renders a component's template in a different part of the DOM (outside the component tree) while keeping it logically inside the component. Used for modals, tooltips, and notifications that need to escape overflow/z-index constraints.
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Modal content</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>
Suspense handles async components and async setup() functions. It shows a fallback slot while waiting for async operations to complete. Works with async components (defineAsyncComponent) and components with async setup().
<Suspense>
<template #default>
<AsyncUserProfile /> <!-- has async setup() -->
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
defineAsyncComponent lazily loads a component only when it is needed. Supports loading/error states and timeout configuration.
const UserProfile = defineAsyncComponent({
loader: () => import("./UserProfile.vue"),
loadingComponent: Spinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
});
provide/inject enables dependency injection across the component tree without prop drilling. A parent provides a value; any descendant can inject it. Use with readonly() to prevent mutation from children.
// Parent
const theme = ref("dark");
provide("theme", readonly(theme));
// Any descendant
const theme = inject("theme");
// inject("theme", "light") // with default value
// watchEffect - auto-tracks, runs immediately
watchEffect(() => {
console.log(count.value, name.value); // tracks both
});
// watch - explicit, lazy
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
Custom directives add low-level DOM manipulation behavior to elements. Define with app.directive() globally or locally in a component. Hooks: created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted.
app.directive("focus", {
mounted(el) { el.focus(); }
});
// Usage
<input v-focus />
<input :value="name" @input="name = $event.target.value">
<component v-bind="{ id: 1, class: 'active' }">
In script setup, component internals are private by default. defineExpose() explicitly exposes properties/methods to parent components accessing the component via template ref.
<script setup>
const count = ref(0);
function reset() { count.value = 0; }
defineExpose({ count, reset }); // parent can access these
</script>
// Parent
const childRef = ref();
childRef.value.reset();
const state = reactive({ name: "Alice", age: 30 });
// toRefs - safe destructuring
const { name, age } = toRefs(state);
name.value = "Bob"; // still reactive
Plugins extend Vue functionality globally. A plugin is an object with an install() method or a function. Use app.use(plugin, options) to install. Common plugins: Vue Router, Pinia, i18n, custom UI libraries.
// Plugin definition
const myPlugin = {
install(app, options) {
app.config.globalProperties.$translate = (key) => options[key];
app.component("MyComponent", MyComponent);
app.directive("focus", focusDirective);
}
};
app.use(myPlugin, { hello: "Bonjour" });
<input v-model.lazy="search" /> <!-- syncs on blur/enter -->
<input v-model.number="age" /> <!-- converts to number -->
<input v-model.trim="username" /> <!-- trims whitespace -->
KeepAlive caches component instances when they are toggled off, preserving their state and avoiding re-mounting. Use include/exclude to control which components are cached. Cached components trigger onActivated/onDeactivated hooks.
<KeepAlive :include="['UserList', 'Dashboard']" :max="10">
<component :is="currentView" />
</KeepAlive>
The Transition component applies enter/leave animations to a single element or component. Uses CSS classes (v-enter-from, v-enter-active, v-enter-to, v-leave-from, v-leave-active, v-leave-to) or JavaScript hooks.
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
<!-- Child: scoped slot -->
<slot :item="item" :index="index" />
<!-- Parent: receives slot data -->
<template #default="{ item, index }">
<span>{{ index }}: {{ item.name }}</span>
</template>
useTemplateRef() (Vue 3.5+) is the modern way to get a reference to a template element or component. It replaces the pattern of declaring a ref with the same name as the template ref attribute.
<script setup>
import { useTemplateRef, onMounted } from "vue";
const inputEl = useTemplateRef("myInput");
onMounted(() => inputEl.value.focus());
</script>
<template>
<input ref="myInput" />
</template>
// Global
app.component("MyButton", MyButton);
// Local (script setup - just import)
import MyButton from "./MyButton.vue";
// MyButton is automatically available in template
Reactivity Transform was an experimental feature that allowed using reactive variables without .value (using $ref, $computed macros). It was removed in Vue 3.4 due to confusion about when .value is needed. The standard ref() with .value is the recommended approach.
Validated emits use defineEmits with an object syntax to validate event payloads at runtime. Unvalidated emits just declare event names as strings. Validation helps catch bugs during development.
const emit = defineEmits({
// No validation
click: null,
// With validation
submit: (payload) => {
if (!payload.email) {
console.warn("submit event missing email");
return false;
}
return true;
}
});
Nuxt.js is a meta-framework built on Vue that adds: file-based routing, SSR/SSG/hybrid rendering, auto-imports (no need to import ref, computed, etc.), server API routes, layouts, middleware, and optimized production builds. It is to Vue what Next.js is to React.
v-memo memoizes a sub-tree of the template. It skips re-rendering the element and its children when the specified dependency array has not changed. Useful for optimizing large v-for lists.
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<!-- Only re-renders when item.id or item.selected changes -->
<p>{{ item.name }}</p>
<ExpensiveComponent :data="item" />
</div>
app.config.errorHandler = (err, instance, info) => {
Sentry.captureException(err);
console.error(err);
};
useAttrs() returns non-prop attributes passed to the component (class, style, event listeners not declared as props). useSlots() returns the slots object. Both are available in script setup without importing.
<script setup>
const attrs = useAttrs(); // { class, style, onCustomEvent }
const slots = useSlots(); // { default, header }
// Disable automatic attribute inheritance
defineOptions({ inheritAttrs: false });
</script>
import { mount, shallowMount } from "@vue/test-utils";
const wrapper = mount(UserCard, {
props: { user: { name: "Alice" } }
});
expect(wrapper.text()).toContain("Alice");
<script setup>
const inputEl = ref(null); // template ref
onMounted(() => inputEl.value.focus());
</script>
<template>
<input ref="inputEl" />
</template>
defineModel() (Vue 3.4+) simplifies implementing v-model on a component. It creates a two-way binding automatically, replacing the manual defineProps + defineEmits pattern for v-model.
<script setup>
const model = defineModel(); // replaces modelValue prop + update:modelValue emit
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>
<!-- Parent -->
<MyInput v-model="username" />
onServerPrefetch(async () => {
// runs on server only
data.value = await fetchData();
});
onMounted(() => {
// runs on client only
initThirdPartyLib();
});
// Legacy (globalProperties)
app.config.globalProperties.$http = axios;
// Modern (provide/inject)
app.provide("http", axios);
// In component:
const http = inject("http");
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
<style>
.list-enter-active, .list-leave-active { transition: all 0.3s; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
.list-move { transition: transform 0.3s; }
</style>
Explore 500+ free tutorials across 20+ languages and frameworks.