· frameworks  · 6 min read

10 Hidden Features of Vue.js That Will Transform Your Development

Uncover 10 lesser-known Vue.js features - from v-memo and effectScope to defineExpose and defineCustomElement - that can boost performance, simplify component APIs, and make your code more maintainable.

Uncover 10 lesser-known Vue.js features - from v-memo and effectScope to defineExpose and defineCustomElement - that can boost performance, simplify component APIs, and make your code more maintainable.

Introduction

Imagine shipping UI that’s faster, cleaner, and easier to reason about - without rewriting your app. In this post you’ll learn 10 lesser-known Vue.js features that do exactly that. Each one is practical, backed by small examples, and comes with tips for when to use it and what to watch out for. Read one section, apply it, and you’ll immediately feel the payoff.

1) v-memo - skip expensive re-renders

Why it helps: v-memo lets Vue skip patching parts of the template when the memo key hasn’t changed. Use it to optimize expensive static-ish subtrees in large lists or complex UIs.

Example:

<template>
  <div v-for="item in items" :key="item.id">
    <HeavyChart :data="item.chartData" v-memo="item.chartHash" />
  </div>
</template>

Quick tips:

  • Provide a stable memo key (string/number/array). If the key is unchanged, Vue will skip diffing that vnode.
  • Great for components that are pure functions of a few props.

Pitfalls:

  • Incorrect memo keys cause stale UI. Only memoize when you can accurately determine the conditions that require an update.

Docs: https://vuejs.org/guide/optimizations/preventing-unnecessary-renders.html#v-memo

2) effectScope - manage reactive lifecycles cleanly

Why it helps: effectScope lets you create and tear down groups of reactive effects (computed, watch, watchers) programmatically. Perfect for composables that create internal reactive state and need explicit cleanup.

Example:

import { effectScope, reactive } from 'vue';

function useTemporaryStore() {
  const scope = effectScope();
  let store;

  scope.run(() => {
    store = reactive({ x: 0 });
    // start watchers/computeds here
  });

  return {
    store,
    stop: () => scope.stop(),
  };
}

When to use:

  • In composables that subscribe to external resources.
  • For dynamic components that spin up reactive logic only while active.

Docs: https://vuejs.org/guide/extras/reactivity-in-depth.html#effect-scope

3) defineExpose - selectively expose internals to parent refs

Why it helps: When a parent gets a ref to a child component (via ref=“child”), Vue only exposes public properties. defineExpose lets you choose what the parent can call - good for controlled APIs.

Example (script setup):

<script setup>
  import { ref } from 'vue';

  const localCount = ref(0);

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

  defineExpose({ increment });
</script>

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

Parent usage:

<Child ref="childRef" />
<!-- in parent script -->
childRef.value.increment()

Tips:

  • Use for imperative APIs (focus(), reset()).
  • Avoid exposing reactive internals you didn’t intend to be part of the public contract.

Docs: https://vuejs.org/guide/components/instance.html#defineexpose

4) shallowRef / shallowReactive / markRaw - control reactivity for performance

Why it helps: Vue’s reactivity is deep by default. When you hold large immutable structures (maps, 3rd-party objects like Map/Set, or big parsed trees), deep tracking is expensive and often unnecessary.

Patterns:

  • shallowRef(value) - only the ref wrapper is reactive; the nested object is not made reactive.
  • shallowReactive(obj) - only the root properties are reactive.
  • markRaw(obj) - exclude an object entirely from reactivity.

Example:

import { shallowRef, markRaw } from 'vue';

const bigImmutable = markRaw(parseLargeJson());
const widget = shallowRef(bigImmutable);

When to use:

  • Third-party libraries that break when proxied (e.g., some chart libraries).
  • Large immutable caches or graphs.

Tip:

  • Use carefully: you trade reactivity for performance. Document why you chose to avoid reactivity.

Docs: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#shallowreactivity

5) watch + onInvalidate - cancel and debounce async effects

Why it helps: Asynchronous watchers often race. onInvalidate lets you register a cleanup for the previous run - ideal for debouncing API calls or canceling fetches.

Example:

import { watch } from 'vue';

watch(
  () => searchQuery.value,
  async (q, _, onInvalidate) => {
    let cancelled = false;
    onInvalidate(() => {
      cancelled = true;
    });

    const result = await fetchResults(q);
    if (!cancelled) results.value = result;
  }
);

Best practice:

  • Use onInvalidate to abort fetches (AbortController) or set a cancellation flag.
  • Avoid side effects without cleanup when the watched source changes rapidly.

Docs: https://vuejs.org/guide/essentials/watchers.html#cleanup-and-oninvalidate

6) Teleport - DOM placement made simple

Why it helps: Teleport lets a component render its markup outside of its DOM hierarchy - ideal for modals, tooltips, context menus, or any element that must live at top-level for z-index/overflow reasons.

Example:

<template>
  <teleport to="body">
    <div class="modal">I live in document.body</div>
  </teleport>
</template>

Tips:

  • Teleport preserves reactivity and event handling.
  • You can teleport to any CSS selector, even containers inside shadow DOM.

Docs: https://vuejs.org/guide/built-ins/teleport.html

7) Suspense - graceful async UI and async setup

Why it helps: Suspense enables you to present fallbacks while components (or async setup) load - perfect for code-splitting, skeletons, and avoiding flashes of empty content.

Example:

<Suspense>
  <template #default>
    <AsyncUserProfile :id="id" />
  </template>
  <template #fallback>
    <UserSkeleton />
  </template>
</Suspense>

Notes:

  • Works with async setup or components returned by defineAsyncComponent.
  • Combine with router-level code splitting for snappier perceived performance.

Docs: https://vuejs.org/guide/built-ins/suspense.html

8) Advanced v-model (multiple models & modifiers)

Why it helps: Vue 3 allows multiple v-model bindings with arguments and custom modifiers - build cleaner wrapper components without custom events or verbose prop wiring.

Example:

<!-- Child.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
  <input :value="email" @input="$emit('update:email', $event.target.value)" />
</template>

<script setup>
  defineProps(['modelValue', 'email']);
</script>

<!-- Parent usage -->
<MyComponent v-model:modelValue="name" v-model:email="emailAddress" />

Modifiers:

  • Use v-model.trim or custom modelModifiers on components to pre-process values.

When to use:

  • Building form controls and wrappers that naturally have several bindable pieces of state.

Docs: https://vuejs.org/guide/components/v-model.html

9) toRef / toRefs / toRaw - safe prop access and destructuring

Why it helps: Destructuring props or reactive objects breaks reactivity if done naively. toRef and toRefs give you references that stay linked to the original reactive source.

Example (prop destructuring safely):

<script setup>
const props = defineProps({ count: Number })
const countRef = toRef(props, 'count')

// Now countRef.value stays reactive with props.count
</script>

toRaw:

  • Use toRaw to access the underlying non-proxied object (rare, but useful for comparisons or third-party interop).

Pitfall:

  • Don’t overuse toRaw; it breaks reactivity when you mutate the raw object.

Docs: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#toref-torefs

10) defineCustomElement / Vue as Web Components

Why it helps: Vue can compile components into standards-based Web Components (custom elements). Ship a UI component that works in any app - framework-agnostic reuse and easier embedding in legacy systems.

Example (basic idea):

import { defineCustomElement } from 'vue';
import MyWidget from './MyWidget.ce.vue';

const el = defineCustomElement(MyWidget);
customElements.define('my-widget', el);

When to use:

  • When you need framework-agnostic widgets for CMSs or legacy pages.
  • If your organization needs a single source of UI components used across non-Vue projects.

Caveats:

  • Bundle size and style encapsulation considerations; evaluate CSS strategy.
  • Not all Vue features map 1:1 to custom element boundaries (check lifecycle differences).

Docs: https://vuejs.org/guide/extras/web-components.html


Conclusion - pick one and ship

You don’t need to apply all ten at once. Pick one that solves a current pain point and try it in a small PR. v-memo or shallowRef often yield immediate performance wins. effectScope and defineExpose clean up interfaces and lifecycles. Teleport and Suspense improve UX with minimal code.

Try one change this week. Measure the impact. Then add another. Over time these “hidden” tools will transform your development experience and the resilience of your codebase.

Further reading

Back to Blog

Related Posts

View All Posts »
Mastering Hapi.js: 10 Tips to Boost Your API Performance

Mastering Hapi.js: 10 Tips to Boost Your API Performance

Learn 10 practical, high-impact techniques to make your Hapi.js APIs faster, leaner, and more scalable. Covers Catbox caching, response ETags, streaming, database patterns, validation tuning, plugin design, metrics, clustering, and avoiding event-loop blocking.

Maximizing Performance: Astro's Secret Weapon

Maximizing Performance: Astro's Secret Weapon

Discover lesser-known Astro optimizations - from smart hydration directives to critical CSS inlining, image strategies, and build-time tricks - that can make your static sites feel instant.