DEV Community

Cover image for πŸ§‘β€πŸ³ Cooking with Vue 3: SFC β€” Features you might not know about
Dmitrii Zakharov
Dmitrii Zakharov

Posted on • Edited on

πŸ§‘β€πŸ³ Cooking with Vue 3: SFC β€” Features you might not know about

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 in <script setup>

Vue 3 offers powerful enhancements to how v-model is used within the <script setup> context β€” including support for multiple models and modifiers.

β€· Multiple v-model bindings

You can define multiple v-models by naming each model explicitly:

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

βœ… Eliminates the need to manually declare props and emit events for model binding.

β€· v-model modifiers (built-in & custom)

Modifiers let you transform input data automatically β€” great for building reusable, format-aware input components.

Built-in modifiers:

  • .trim β€” trims whitespace
  • .number β€” converts value to a number
  • .lazy β€” updates on change instead of input

Example usage:

<input v-model.number.lazy="filterId" type="text" />
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The input is converted to a number
  • The update happens on change, not on every keystroke

β€· Custom modifiers with defineModel

You can also define your own modifiers using the [model, modifiers] destructuring pattern:

<template>
  <input v-model.date="filterAge" type="date" />
</template>

<script lang="ts" setup>
const [model, modifiers] = defineModel<string>({
  set(value) {
    if (modifiers.date) {
      return convertDate(value); // Custom conversion logic
    }
    return value;
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

βœ… Pushes formatting logic into the component layer

βœ… Keeps templates clean and declarative

βœ… Makes base components more reusable and flexible


🧩 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! πŸš€

Top comments (2)

Collapse
 
modelair profile image
modelair • Edited

v-model with modifiers

where? tema ne raskrita

Collapse
 
senior-debugger profile image
Dmitrii Zakharov

Good point, mate! I believe the issue is resolved now.