Back to Vue tutorials
Basic45 min read

Vue Essential Concepts: A Complete Knowledge Guide

Explore essential Vue.js concepts with clear explanations, practical code examples, key reminders, and interview reference cards.

Core Architecture

1. What is Vue.js, and why is it important?

Vue.js is a progressive JavaScript framework used for building user interfaces. Unlike monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. Its core library focuses solely on the view layer, making it easy to integrate with other libraries or existing projects.

Vue combines the declarative templating model of Angular with the virtual DOM and component-driven architecture of React, delivering a developer-friendly framework that scales from small widgets to large-scale single-page applications.

  • Progressive, incrementally adoptable views
  • Declarative data binding and reactivity
  • Lightweight Virtual DOM implementation

2. Explain Vue's reactivity system (Vue 3 ref vs reactive).

Vue 3 reactivity is powered by JavaScript Proxies. When a component renders, Vue tracks which reactive properties are accessed and registers them as dependencies. When these properties change, Vue automatically schedules a re-render of the components that depend on them.

`ref` is used for defining reactive references of any type (primitives like string, number, or objects). It wraps the inner value inside a reactive container with a single `.value` property.

`reactive` is used exclusively for objects and arrays. It returns a reactive Proxy of the original object directly. You cannot reassign a `reactive` object or destructure it without losing its reactivity tracker.

  • Proxy-based system detects property additions and deletions
  • ref requires .value in script, unwraps automatically in templates
  • reactive is strictly for objects/arrays and doesn't use .value
import { ref, reactive } from 'vue';

// ref usage
const count = ref(0);
count.value++; // Increment value

// reactive usage
const state = reactive({ name: 'Niyazi', role: 'Developer' });
state.role = 'Elite Developer'; // Directly update

3. What is the difference between the Composition API and the Options API?

The Options API organizes component code by options: `data`, `methods`, `computed`, `watch`, and lifecycle hooks. The Composition API organizes code by logical concerns using functions inside a unified `setup()` hook (or `<script setup>`).

While the Options API is highly readable for simple components, the Composition API scale-up is much better for large applications. It allows you to group related features together, extract them into reusable functions called "composables", and provides out-of-the-box TypeScript support.

  • Options API splits logic across pre-defined configurations
  • Composition API groups code by feature concern
  • Composables replace mixins, preventing namespace collisions
<script setup>
import { ref, onMounted } from 'vue';

const items = ref([]);
onMounted(async () => {
  items.value = await fetch('/api/items').then(r => r.json());
});
</script>

Data flow and transformations

4. How do computed properties differ from watchers?

Computed properties (`computed`) are derived reactive values. They are cached based on their reactive dependencies, meaning they only re-evaluate when one of their source dependencies changes. Computed properties must be pure functions with no side effects.

Watchers (`watch`) are used to perform asynchronous or expensive side effects in response to data changes. They do not return a value, but instead execute a callback function where you can perform operations like network requests or DOM modifications.

  • Computed properties cache results and should be pure
  • Watchers trigger side effects on value changes
  • Computed is synchronous; watch can be asynchronous
import { ref, computed, watch } from 'vue';

const price = ref(100);
const quantity = ref(2);

// Computed property
const total = computed(() => price.value * quantity.value);

// Watcher
watch(total, (newTotal) => {
  console.log('Total changed to:', newTotal);
});

Templates & Directives

5. Explain v-if vs v-show.

`v-if` is conditional rendering. It ensures that the element and its children are completely destroyed and recreated when the condition changes. If the condition is false on initial render, the element is not rendered at all.

`v-show` is conditional visibility. The element is always rendered and remains in the DOM tree; Vue simply toggles the CSS `display` property (`block` / `none`) to show or hide it.

Use `v-if` when the condition changes rarely, as it has lower initial load costs. Use `v-show` when the element is toggled frequently to avoid DOM creation overhead.

  • v-if adds/removes elements from the DOM
  • v-show toggles CSS display property
  • v-if supports template grouping, v-show does not
<!-- v-if template rendering -->
<div v-if="isLoaded">Rich data panel</div>
<div v-else>Loading...</div>

<!-- v-show style toggling -->
<div v-show="isVisible">Popup tooltip</div>

6. How does v-model work under the hood?

`v-model` is syntax sugar for two-way data binding on form inputs. Under the hood, it combines a dynamic prop binding (`v-bind`) and an event listener (`v-on`).

For a standard text input, `<input v-model="text">` is equivalent to `<input :value="text" @input="text = $event.target.value">`.

In Vue 3, you can use `v-model` on custom components, where it binds to a prop named `modelValue` and listens for an `update:modelValue` event.

  • Combines property binding and input event listeners
  • In custom components, defaults to modelValue prop
  • Supports multiple v-model bindings on a single component
<!-- Short syntax -->
<CustomInput v-model="username" />

<!-- Long equivalent -->
<CustomInput
  :modelValue="username"
  @update:modelValue="username = $event"
/>

7. What is the significance of the key attribute in v-for?

The `key` attribute provides a unique identifier for elements in a rendered list. When Vue updates a list rendered with `v-for`, it uses a virtual DOM patching algorithm. By default, it applies an "in-place patch" strategy, but when elements reorder, this can lead to incorrect state tracking.

Providing a stable, unique `key` (such as a database ID) allows Vue to track each node's identity, ensuring that component state (e.g., input values, focus) is preserved and DOM updates are minimal and correct.

  • Helps Vue's diffing algorithm identify VNodes
  • Ensures component state is correctly matched to array items
  • Avoid using array index as keys when list items can reorder
<!-- Correct key assignment -->
<ul>
  <li v-for="user in users" :key="user.id">
    {{ user.name }}
  </li>
</ul>

Component communication

8. What are scoped slots and how do they work?

Slots are placeholders in a component template that allow a parent component to inject custom markup. Scoped slots take this further by allowing the child component to pass data back up to the parent template when rendering the slot content.

This acts like a function parameter: the child component decides *when* and *what* data to provide, while the parent component defines *how* that data should be styled and structured.

  • Child binds attributes to slot outlet
  • Parent receives slot properties as an object parameter
  • Essential pattern for building highly configurable list/table views
<!-- Child: List.vue -->
<slot name="item" :item="currentRecord"></slot>

<!-- Parent component -->
<List>
  <template #item="{ item }">
    <span class="highlight">{{ item.title }}</span>
  </template>
</List>

9. Explain Provide and Inject in Vue.

`provide` and `inject` allow an ancestor component to serve as a dependency provider for all its descendants, regardless of how deep the component tree is. This avoids the problem of "prop drilling" where intermediary components have to pass down props they do not use.

To keep provided values reactive, you should pass a reactive property (like a `ref`). For safety, it is recommended to provide read-only references and provide updating methods from the ancestor to prevent children from modifying state directly.

  • Bypasses prop drilling for deeply nested components
  • Provide is defined in parent; inject is consumed in children
  • Reactively updates across the component tree when using refs
// Ancestor Component
import { provide, ref, readonly } from 'vue';
const theme = ref('dark');
provide('theme', readonly(theme));
provide('toggleTheme', () => theme.value = theme.value === 'dark' ? 'light' : 'dark');

// Descendant Component
import { inject } from 'vue';
const theme = inject('theme');
const toggleTheme = inject('toggleTheme');

Virtual DOM & Compile Optimizations

10. What is Vue's Virtual DOM and how does reconciliation work?

Vue creates a tree of Virtual Nodes (VNodes) in memory. When state updates, it generates a new VNode tree and diffs it against the old one, batching minimal updates to the real DOM.

Vue 3 improves on traditional diffing by using compiler-informed optimizations. It statically analyzes templates during compilation to tag dynamic expressions with 'patch flags' (e.g., dynamic text, class, or style) and hoists static subtrees. During updates, the diffing algorithm skips static nodes and directly targets dynamic nodes, achieving near-native rendering speeds.

  • Virtual DOM reduces expensive operations on real DOM nodes
  • Patch flags focus updates on dynamic values
  • Static hoisting prevents recreation of unchanging VNodes

Lifecycle & State Management

11. Explain the lifecycle hooks in Vue 3 (Composition API).

Vue components go through a structured lifecycle from creation to destruction. In the Composition API, you import and call lifecycle hooks directly inside the `setup` method.

`setup` serves as the entry point and runs before the component is created, replacing `beforeCreate` and `created`.

`onMounted` runs after the component is rendered on screen (perfect for API requests or DOM measurements).

`onBeforeUnmount` runs before the component instance is destroyed, making it the correct place to clean up timers, resize observers, or custom window event listeners.

  • setup replaces traditional creation hooks
  • onMounted handles side effects requiring DOM access
  • onBeforeUnmount prevents memory leaks by handling cleanups
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';

const handleResize = () => console.log('Resized');

onMounted(() => {
  window.addEventListener('resize', handleResize);
});

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize);
});
</script>

12. How does Vue Router handle navigation guards?

Vue Router handles page navigation in Single Page Applications. Navigation guards are callbacks that intercept transitions, allowing you to cancel, redirect, or modify page flows.

`beforeEach` runs globally before any navigation resolve. This is commonly used for authentication checks, checking meta fields on the route to see if a login token is required.

`beforeRouteLeave` runs inside components when navigating away, which is useful for prompting users to save unsaved draft form values.

  • beforeEach guards global routes based on authentication
  • beforeEnter guards specific routes in the route config
  • beforeRouteLeave prompts user before unsaved exit
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login' };
  }
});

13. What is Pinia, and how does it compare to Vuex?

Pinia is the modern, lightweight state management library for Vue. It has replaced Vuex as the recommended tool. It simplifies state management by removing mutations: actions directly modify state, removing boilerplate code.

Pinia has full TypeScript type inference and support, and rather than a single massive state tree with namespaces, Pinia features a modular design where you define flat, independent stores that can be imported and loaded on demand.

  • Eliminates mutations, simplifying code to state, getters, actions
  • Native, robust TypeScript support out-of-the-box
  • Modular structure loads only required stores
import { defineStore } from 'pinia';

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  getters: {
    itemCount: (state) => state.items.length,
  },
  actions: {
    addItem(item) {
      this.items.push(item);
    },
  },
});

Advanced Optimization & Testing

14. How do you optimize Vue application performance (e.g. v-once, v-memo)?

Vue apps are optimized by minimizing rendering and bundle size.

`v-once` caches the element and renders it only once, skipping all updates. `v-memo` conditionally caches a template block and only patches it if specified dependencies change.

`keep-alive` caches component instances in memory during tab/view switches to avoid reloading. `defineAsyncComponent` lazy-loads components dynamically, splitting the bundle and reducing initial page load times.

  • v-once skips updates for static parts of templates
  • v-memo avoids diffing large component structures conditionally
  • defineAsyncComponent splits code and lazy loads bundles
import { defineAsyncComponent } from 'vue';

// Lazy load component
const HeavyDashboard = defineAsyncComponent(() =>
  import('./components/HeavyDashboard.vue')
);

15. How do you test Vue components (Vitest & Vue Test Utils)?

Vue Test Utils is the official library for unit testing Vue components. It mounts components in a virtual jsdom environment, allowing you to feed in props, stub dependencies (like Vue Router or Pinia), trigger interactions, and assert the output.

`mount` creates the component and renders all its children, while `shallowMount` renders only the component container, replacing children with stubs for isolated testing.

When writing tests, wait for reactive updates to settle using `await wrapper.setValue()` or `await flushPromises()` before verifying elements.

  • Unit tests verify inputs (props/events) yield correct HTML
  • shallowMount isolates component under test from dynamic children
  • Wait for reactivity to resolve before asserting changes
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

test('increments count on click', async () => {
  const wrapper = mount(Counter);
  expect(wrapper.text()).toContain('0');

  await wrapper.find('button').trigger('click');
  expect(wrapper.text()).toContain('1');
});

Templates & Directives

16. What is a Single-File Component (SFC)?

A Single-File Component (SFC) is a Vue file with a `.vue` extension that encapsulates the template, JavaScript logic, and CSS styles of a component in a single file.

This keeps logical units cohesive and self-contained. SFCs are compiled by build tools (like Vite or Webpack with vue-loader) into standard JavaScript modules, supporting features like scoped CSS, preprocessors, and hot module replacement.

  • Keeps HTML template, logic, and styles in a single .vue file
  • Supports scoped CSS to prevent styling leaks
  • Requires compilation via Vite or Webpack vue-loader
<template>
  <button class="btn">{{ label }}</button>
</template>

<script setup>
defineProps({ label: String });
</script>

<style scoped>
.btn { border-radius: 4px; }
</style>

Component communication

17. Why must component data be a function in Options API?

In the Options API, the data property must be a function that returns a fresh data object instance, rather than a plain object directly.

This is because JavaScript objects are passed by reference. If the data property were a plain object, every instance of the component would share the same data object in memory. A change in one instance would immediately affect all other instances.

  • data is a function returning a new object
  • Prevents shared state reference bugs across component instances
  • Ensures isolated scope for each reusable instance
export default {
  data() {
    return {
      localCount: 0 // Fresh reference per instance
    };
  }
};

Advanced Optimization & Testing

18. What is nextTick(), and when should you use it?

nextTick() is a utility that returns a Promise that resolves after Vue has updated the DOM in response to reactive state changes. Vue batches reactive updates and processes them asynchronously to improve performance.

Use nextTick() when you need to execute code immediately after the DOM has been updated, such as focusing a new input element, measuring an element's dimensions, or scrolling to new content.

  • Waits for Vue asynchronous DOM update queue to flush
  • Returns a Promise that is awaitable
  • Essential for DOM manipulation immediately after state changes
import { ref, nextTick } from 'vue';

const showInput = ref(false);
const inputRef = ref(null);

async function focusInput() {
  showInput.value = true;
  await nextTick(); // Wait for input to be created in DOM
  inputRef.value.focus();
}

Core Architecture

19. Explain mixins and why Composition API composables are preferred.

Mixins are an Options API feature used to distribute reusable component options. When a component uses a mixin, all options in the mixin are merged into the component.

However, mixins suffer from major drawbacks: implicit dependencies, namespace collisions (if two mixins define the same method name), and unclear source of properties. In Vue 3, Composition API composables (custom hooks) are preferred because they make state source explicit and avoid name collisions.

  • Mixins merge options into components implicitly
  • Namespace collisions and hidden state origins cause maintenance overhead
  • Composables offer explicit imports, returns, and clear logic tracking
// useMouse.ts (Composable)
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  const update = (e) => { x.value = e.pageX; y.value = e.pageY; };
  onMounted(() => window.addEventListener('mousemove', update));
  onBeforeUnmount(() => window.removeEventListener('mousemove', update));
  return { x, y };
}

20. What is a Vue plugin, and how do you write and install one?

A Vue plugin adds global-level functionality to a Vue application. It is defined as an object with an install() method (or as a simple function) that receives the app instance and any custom options.

Plugins are installed by calling app.use(plugin) during the application bootstrapping phase. They are commonly used to register global directives, inject global helpers, or integrate third-party libraries (e.g. Pinia, Vue Router).

  • Object containing an install(app, options) method
  • Installed globally using app.use()
  • Commonly registers global directives, properties, or providers
// loggerPlugin.js
export const loggerPlugin = {
  install(app, options) {
    app.config.globalProperties.$log = (msg) => console.log('[App]:', msg);
  }
};

// main.js
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).use(loggerPlugin).mount('#app');

21. What is a Vue application instance, and how is it created in Vue 3?

In Vue 3, a Vue application instance is created using the `createApp` function. This instance represents the application context and provides methods to configure global options, plugins, directives, and components before mounting to a real DOM element.

This is a major improvement over Vue 2 (which used the global `new Vue` constructor) because it allows multiple independent application instances to run on the same page without sharing global configuration.

  • Created using createApp(RootComponent)
  • Provides clean context separation compared to Vue 2 global Vue constructor
  • Must be mounted to a DOM container using app.mount("#app")
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app');

Component communication

22. What is the difference between local and global components?

Local components are imported directly into the component file where they are used and must be declared in Options API or simply used directly in Composition API `<script setup>`.

Global components are registered on the application instance using `app.component('Name', Component)`. They are available in any template without being imported, but they increase bundle size because build tools cannot tree-shake unused global components.

  • Local components are imported where needed, enabling tree-shaking
  • Global components are registered via app.component() and don't need imports
  • Use local components by default to optimize bundle size and track dependencies
// Global registration
app.component('BaseButton', BaseButton);

// Local registration (script setup compiles automatically)
import CustomCard from './CustomCard.vue';

Advanced Optimization & Testing

23. What is the difference between synchronous and asynchronous components?

Synchronous components are imported directly and included in the main JavaScript bundle, meaning they load and block initial execution until resolved. Asynchronous components are loaded dynamically on demand using `defineAsyncComponent`.

This splits components into separate chunks that are only loaded over the network when needed, reducing initial bundle size and improving page load performance (especially for modals, heavy drawers, or dashboard tab contents).

  • Sync components are bundled together and load instantly
  • Async components load dynamic chunks using defineAsyncComponent()
  • Implements code splitting for off-screen or conditionally rendered views
import { defineAsyncComponent } from 'vue';

const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
);

Templates & Directives

24. How do you handle form validation in a Vue.js application?

Form validation in Vue can be handled manually using reactive variables and computed properties to validate input binds on the fly, or by using dedicated validation libraries like VeeValidate or Vuelidate.

VeeValidate uses declarative components or schema validation (like Zod), while Vuelidate uses model-driven validation rules. Client-side validation must always be paired with server-side validation for robust security.

  • Manual computed validation handles simple inputs
  • Libraries like VeeValidate/Vuelidate manage complex multi-field rules
  • Pair client-side form validation with server-side security checks
import { ref, computed } from 'vue';
const email = ref('');
const emailError = computed(() => {
  if (!email.value) return 'Required';
  if (!email.value.includes('@')) return 'Invalid format';
  return '';
});

Component communication

25. What is $parent, and why is its direct usage discouraged?

$parent is an instance property that references the parent component instance of the current component. While it provides direct access to parent state and methods, using it creates tight coupling between child and parent.

This breaks the "props down, events up" design pattern, making child components brittle (they will crash if placed in a different parent context) and extremely difficult to test in isolation.

  • Exposes the parent component instance directly to the child
  • Causes tight coupling and breaks component reusability
  • Use props and emitted events instead to maintain clean separation
// Discouraged:
this.$parent.someMethod();

// Encouraged:
const emit = defineEmits(['trigger-action']);
emit('trigger-action');

Core Architecture

26. How does Vue differ from React and Angular?

Vue is a progressive framework with HTML-based templates and built-in reactivity. React is a UI library commonly using JSX and explicit state updates. Angular is a broader, more opinionated framework with dependency injection and a larger built-in platform. The right choice depends on team skills, architecture, ecosystem, and product constraints.

  • Vue balances gradual adoption with framework conventions
  • React provides a flexible UI-library model
  • Angular supplies a strongly structured application platform

Component communication

27. How do props pass and validate parent data?

Props carry data from a parent to a child through one-way data flow. Children should treat props as read-only and request changes by emitting events. Runtime declarations can specify type, required status, defaults, and validators; TypeScript declarations provide compile-time contracts.

  • Parent data flows down through props
  • Object and array defaults use factory functions
  • Runtime validation warns during development
<script setup lang="ts">
const props = defineProps<{ title: string; count?: number }>();
</script>

28. How does a child communicate with its parent?

A child declares and emits custom events, optionally carrying a payload. The parent listens and owns the resulting state change. This preserves the common one-way pattern: props down and events up.

  • Declare events with defineEmits()
  • Emit descriptive domain events rather than parent method names
  • Validate or type event payloads
const emit = defineEmits<{ save: [item: Item] }>();
emit('save', item);

Templates & Directives

29. What are Vue directives and event modifiers?

Directives are v-prefixed template attributes that apply reactive behaviour. Common examples include v-bind, v-on, v-model, v-if, v-show, v-for, v-html, v-text, v-once, and v-memo. Event modifiers such as .prevent, .stop, .once, .self, and .passive express DOM event behaviour declaratively.

  • Colon is shorthand for v-bind
  • @ is shorthand for v-on
  • Treat v-html input as an XSS-sensitive sink
<form @submit.prevent="save">
  <button :disabled="saving">Save</button>
</form>

Component communication

30. How do default, named, and scoped slots differ?

A default slot accepts unnamed parent content. Named slots provide several labelled layout regions. Scoped slots let the child expose data while the parent controls its rendering. Slots support reusable layout components without hard-coding their inner markup.

  • Default slots accept ordinary child content
  • Named slots address several outlets
  • Scoped slots expose child data to parent-authored markup

Lifecycle & State Management

31. Where should initial component data be fetched?

Start independent client requests during setup, or use onMounted() when the work depends on mounted DOM or browser-only APIs. Avoid fetching in onUpdated(), where changing state can trigger request loops. Nuxt applications should use framework data APIs when server rendering or request deduplication is required.

  • Use onMounted for DOM-dependent work
  • Clean up obsolete requests when dependencies change
  • Use Nuxt data primitives for SSR-aware fetching

Data flow and transformations

32. How do watch() and watchEffect() differ?

watch() observes explicit sources, is lazy by default, and provides old and new values. watchEffect() runs immediately and automatically tracks reactive values read synchronously during its execution. Both support cleanup for stale asynchronous effects.

  • watch gives precise dependency control
  • watchEffect discovers dependencies automatically
  • Cancel stale requests during cleanup
watch(searchTerm, async (term, _oldTerm, onCleanup) => {
  const controller = new AbortController();
  onCleanup(() => controller.abort());
  results.value = await search(term, controller.signal);
});

33. What does toRefs() solve?

Ordinary destructuring reads values from a reactive object and can disconnect those local variables from property reactivity. toRefs() creates property refs linked to the original object, allowing safe destructuring and two-way updates.

  • Use toRefs when exposing a reactive object from a composable
  • Each returned ref remains linked to its source property
  • Do not add it when direct property access is clearer

Lifecycle & State Management

34. How should legacy Vuex stores be understood and tested?

Vuex organises shared state into state, getters, synchronous mutations, asynchronous actions, and optionally namespaced feature modules. Helpers such as mapState expose store values as computed properties, while mutation-name constants are optional. Test a mutation as a plain function with known state and payload. Pinia is recommended for new Vue applications.

  • State stores; getters derive; mutations synchronously update; actions coordinate
  • Namespace large stores by feature
  • Test mutations from known input state to expected output state

35. How is Vue Router configured and how do dynamic routes work?

createRouter() combines a history implementation with route records. RouterLink performs client navigation and RouterView renders the match. A colon defines a dynamic segment such as /users/:id; useRoute() reads params, or route props can decouple the page component from the router.

  • Install the router with app.use(router)
  • Lazy-load route components with dynamic import()
  • Prefer route props when practical
const router = createRouter({
  history: createWebHistory(),
  routes: [{ path: '/users/:id', component: () => import('./UserPage.vue'), props: true }],
});

Advanced Optimization & Testing

36. What do Vite, Vue CLI, and vue-loader do?

Vite is the standard modern Vue development and build tool, serving native modules on demand in development and creating optimised production bundles. Vue CLI is a maintenance-mode Webpack toolchain. vue-loader is the Webpack loader that compiles .vue Single-File Components; Vite uses the official Vue plugin instead.

  • Use create-vue with Vite for new projects
  • vue-loader belongs to Webpack pipelines
  • Vue CLI remains relevant in existing applications

37. How do dynamic components and KeepAlive work?

The built-in component element renders whichever component its :is value selects. KeepAlive can cache inactive dynamic component instances so their local state survives view switches. Include and exclude controls should keep caching intentional.

  • Use shallowRef for component definitions selected in script
  • KeepAlive preserves component instances, not just DOM
  • Avoid caching unbounded dynamic views
<KeepAlive>
  <component :is="activeTab" />
</KeepAlive>

38. What does Teleport solve?

Teleport renders a template subtree at another DOM target while retaining its logical Vue component relationship. It is useful when overlays must escape parent overflow, positioning, or stacking contexts.

  • Commonly target body or a dedicated overlay root
  • Events and reactivity remain connected to the source component
  • Manage focus and accessibility for modal content
<Teleport to="body">
  <Modal v-if="open" />
</Teleport>

Data flow and transformations

39. When should shallowRef() be used?

shallowRef() tracks replacement of .value but does not recursively proxy nested object properties. It is useful for large immutable payloads, external library instances, and component definitions that should be replaced as a whole. triggerRef() can notify after intentional deep mutation.

  • Nested mutation does not normally trigger an update
  • Replacing .value does trigger
  • Avoid deep proxying third-party instances

40. How do const and readonly() affect reactive state?

const prevents reassignment of the variable binding but does not make object properties immutable, so a const reactive proxy can still be mutated. readonly() returns a proxy that consumers can observe but should not mutate. Keep mutation methods with the state owner.

  • const protects the reference binding
  • readonly protects the exposed proxy API
  • The owning reactive source can still update

Templates & Directives

41. How are custom directives created?

A custom directive packages low-level DOM behaviour into lifecycle hooks such as created, mounted, updated, and unmounted. The mounted hook runs after the bound element enters the DOM. Prefer components or composables when the concern is not primarily direct element behaviour.

  • Register locally or through app.directive()
  • Clean up listeners in unmounted()
  • Use mounted() for focus and DOM integration
const vFocus = {
  mounted(element) {
    element.focus();
  },
};

Lifecycle & State Management

42. How can a Pinia getter accept an argument?

A Pinia getter cannot directly receive call arguments like a method, but it can return a function that accepts them. The returned function is not memoised independently for every argument, so frequently accessed entities may be better stored in a normalised object or Map.

  • Return a lookup function from the getter
  • Do not assume per-argument computed caching
  • Normalise collections for frequent ID lookup
getters: {
  getUserById: (state) => (id) => state.users.find((user) => user.id === id),
}

Core Architecture

43. How did reactivity change from Vue 2 to Vue 3?

Vue 2 primarily used Object.defineProperty getters and setters, which had limitations around property addition, deletion, array indexes, Map, and Set. Vue 3 uses Proxy for reactive objects, allowing broader operation interception and a more capable standalone reactivity system.

  • Vue 3 detects property addition and deletion
  • Proxy supports Map and Set
  • ref wrappers still provide primitive reactivity

Get In Touch


Ready to discuss your next project? Drop me a message.