🧑‍🍳 Cooking with Vue 3: SFC — Features you might not know about

Dmitrii Zakharov

Dmitrii Zakharov

2025-05-29T17:22:45Z

4 min read

This is not a "getting started" guide — this is an advanced look at what makes Vue 3's Single File Components (SFCs) truly powerful, especially the underused capabilities that even experienced developers often overlook.


🧠 Why SFCs deserve your attention

Single File Components (.vue) are not just a structural convenience — they're a fundamental paradigm in Vue 3. With the addition of the Composition API and script setup, SFCs evolve into a highly expressive, maintainable, and scalable tool.

Vue 3 SFCs are:

  • Compile-time optimized with script setup
  • Type-safe and TS-friendly by design
  • Modular and reactive, from template to style
  • Composable at scale, ideal for large codebases

And that’s just scratching the surface.


💎 Underused Vue 3 SFC superpowers

Here's a curated list of advanced and underutilized features that can greatly improve your Vue 3 experience:


🔁 watchEffect — Auto-reactive side effects

Unlike watch, this runs immediately and reactively whenever any reactive variable used inside the callback function changes.

<script lang="ts" setup>
/** Reactive variables. */
const userCountry = ref<null | string>(null);
const userRegion = ref<null | string>(null);
const userCity = ref<null | string>(null);
const userAddress = ref<null | string>(null);

/** Validate and save data automatically on change. */
watchEffect(() => {
  /** Assume that the functions `validateLocation`, `showError`,
        and `saveLocation` have been defined earlier.
  */
  const isValid = validateLocation({
    address: userAddress.value,
    country: userCountry.value,
    region: userRegion.value,
    city: userCity.value,
  });

  if (!isValid) {
    showError('Location is not valid!');
    return;
  }

  saveLocation();
});
</script>
Enter fullscreen mode Exit fullscreen mode

✅ Perfect for logging, data processing, syncing with external systems, or immediate derived state.


🧼 defineExpose — expose internals to parent

By default, <script setup> hides everything. Use defineExpose() to selectively expose internal functions:

<script lang="ts" setup>
defineExpose({
  innerContainerRef,
  setUserAddress,
});

const userAddress = ref<null | string>(null);
const innerContainerRef = ref<null | HTMLDivElement>(null);

const setUserAddress = (address: string) => {
  userAddress.value = address;
};
</script>
Enter fullscreen mode Exit fullscreen mode

✅ Provides a way to access and interact with a component's internal logic from the outside through its API. For example: custom wrapper components built on top of UI libraries.
⚠️ Use with caution as it is a highly specialized approach that can usually be avoided


🔍 shallowReactive and shallowRef

Use these for performance when you don’t need deep reactivity:

<script lang="ts" setup>
const config = shallowReactive({
  inner: {
    nonReactiveProperty: true,
  },
  reactiveProperty: 123,
});

watch(() => config.reactiveProperty, (value) => {
  // Will work, as `reactiveProperty` is reactive at the shallow level
});

watch(() => config.inner.nonReactiveProperty, (value) => {
  // Will NOT work, as `nonReactiveProperty` is nested and not reactive in shallowReactive
});
</script>
Enter fullscreen mode Exit fullscreen mode

✅ Improves performance when rendering large data trees at the first object level
⚠️ Use with caution, as it may cause unexpected bugs in the application due to limiting reactivity


🔄 customRef — Custom reactive logic

You can intercept .value behavior — great for debouncing, throttling, validation:

<script lang="ts" setup>
const userAddress = customRef((track, trigger) => {
  let value = '';

  return {
    get() {
      track();
      return value;
    },
    set(newVal) {
      /** Assume that the function `validateAddress` is defined elsewhere **/
      const isValid = validateAddress(newVal);

      /** Validate against specific rules **/
      if (!isValid) {
        return;
      }

      value = newVal;

      /** Notify reactivity system **/
      trigger();
    }
  };
});

const validAddress = '1600 Pennsylvania Avenue NW, Washington, DC 20500';
const invalidAddress = '';

// ✅ Will be set because it passes validation
userAddress.value = validAddress;
console.log(userAddress.value === validAddress); // Should be `true`

// ❌ Will not be set because it fails validation rules
userAddress.value = invalidAddress;
console.log(userAddress.value === invalidAddress); // Should be `false`
</script>
Enter fullscreen mode Exit fullscreen mode

🔄 v-model with modifiers in <script setup>

Vue 3 allows you to destructure v-model props:

<script lang="ts" setup>
const modelValue = defineModel<string>();

const setModalValue = (value: string) => {
  modelValue.value = value;
};
</script>
Enter fullscreen mode Exit fullscreen mode

Support multiple models:

<script lang="ts" setup>
const title = defineModel('title');
const isPublished = defineModel<boolean>('published');
</script>
Enter fullscreen mode Exit fullscreen mode

✅ No need to manually wire props and events for v-model anymore


🧩 defineOptions — Compile-time component metadata

Set name, inheritAttrs, and more at compile time:

<script lang="ts" setup>
/**
 * Equivalent to `export default`.
*/
defineOptions({
  name: 'CustomButton',
  inheritAttrs: false
});
</script>
Enter fullscreen mode Exit fullscreen mode

✅ Especially useful when creating wrapper components or building reusable libraries
💡 Can be helpful for components located at CustomButton/index.vue, where the file name doesn’t match the desired component name
ℹ️ Reminder: In SFCs, the component name is automatically inferred from the file name if no explicit name option is set. This can be important when you need to make recursive component calls.


Suspense

Suspense is a built-in Vue 3 component that helps you handle asynchronous operations in your templates gracefully. It allows you to display fallback content (like a loading spinner or message) while waiting for async components or data to load.

Key features

  • Delays rendering of child components until async operations (e.g., async setup(), async components) complete.
  • Displays fallback UI during the loading phase.
  • Supports managing multiple asynchronous dependencies simultaneously.

Usage example

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>

    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup lang="ts">
import AsyncComponent from './AsyncComponent.vue';
</script>
Enter fullscreen mode Exit fullscreen mode

When to use

  • To optimize interface loading by lazy-loading heavy or complex components.
  • For canvas or WebGL components that asynchronously load models or resources.

🎭 Script Logic and template binding in <style>

Use v-bind in styles for fully reactive CSS.

<template>
  <div class="button">
    Button
  </div>
</template>

<script lang="ts" setup>
defineProps<{
  color: string;
}>();
</script>

<style scoped lang="scss">
.button {
  background: #212121;
  color: v-bind(color);
}
</style>
Enter fullscreen mode Exit fullscreen mode

⚠️ Use with caution, as it only supports primitives. The following example will not work.

<template>
  <div class="button">
    Button
  </div>
</template>

<script lang="ts" setup>
defineProps<{
  styles: {
    color: string;
  };
}>();
</script>

<style scoped lang="scss">
.button {
  background: #212121;
  /* ❌ This will not work */
  color: v-bind(styles.color); /* or styles['color'] */
}
</style>
Enter fullscreen mode Exit fullscreen mode

🧠 Final thought

I hope you found this useful and maybe even a bit fun to explore.
If it sparked your interest — stay tuned and consider subscribing to my blog!

I've got a long list of upcoming articles, not only on development and Vue, but also on topics like team leadership, engineering management, and the bigger picture of building great products.

See you in the comments — and thanks for reading! 🚀