DEV Community

Ibrahim Zbib
Ibrahim Zbib

Posted on

Latest Updates in Vue

The objective of this page is to provide a quick and easy reference to some of the feature update from Vue 3.3 to Vue 3.5, onwards in order to modernize, optimize and clean ones codebase with new features and updates to legacy features.

Legacy Macros

defineProps()

Reactive Destructured Props

Props can now be destructured and assigned default values the way objects do from a defineProps call

const {
  foo = 'hello', // optional
  bar // required - see defined type and no default value assigned
} = defineProps<{ foo?: string, bar: string }>()
Enter fullscreen mode Exit fullscreen mode

In order to retain the reactivity of destructured props values, is important to wrap them in getters outside the template

watch(foo /* ... */) // ❌ -> throws error in complie-time, 'foo' by it's self isn't a reactive value
watch(() => foo /* ... */) // ✅ -> wrap 'foo' in getter, in order to make it reactive (behaves like [ref_variable].value)
Enter fullscreen mode Exit fullscreen mode

Typescript

Type Handling with Props

Typescript now handles validation type-checking in compile-time

interface Props { msg?: string labels?: string[] };
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
Enter fullscreen mode Exit fullscreen mode

See More

withDefaults() [Only supported on Typescript]

Alternative to destructuring props like an object, one can keep props wrapped in props object and assign defaults with the withDefaults vue-typescript macro

⚠️ Make sure to wrap default values of props that are objects or arrays in withDefaults with getter function

Typescript

interface Props {
 msg?: string
labels?: string[]
};

const props = withDefaults(
 defineProps<Props>(),
 {
   msg: 'hello',
   labels: () => ['one', 'two'] // array and object defaults must be wrapped in getter func
 }
);
Enter fullscreen mode Exit fullscreen mode

See More

defineEmits()

Some compatibility enhancements made with Typescript

Typescript

The emit function can also be typed using either runtime declaration OR type declaration

Runtime Declaration
<script setup lang="ts">
//runtime
const emit = defineEmits(['change', 'update', 'submit']);
emit('change', 'hello'); // ✅
emit('update', 'world'); // ✅
emit('submit', '!'); // ✅ -> ⚠️ although payload passed in will be ignored by callback func
</script>
Enter fullscreen mode Exit fullscreen mode
Type Declaration
Older Syntax
<script setup lang="ts">
// type-based
const emit = defineEmits<{ (e: 'change', id: number): void (e: 'update', value: string): void (e: 'submit'): void }>();
emit('change', 'hello'); // ✅
emit('update', 'world'); // ✅
emit('submit', '!'); // ❌ -> throws error in complie-time, no payload expected
emit('submit'); // ✅ -> no payload passed in, as expected
</script>
Enter fullscreen mode Exit fullscreen mode
Newer Syntax
<script setup lang="ts">
// type-based
// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
change: [id: number]
update: [value: string]
submit: []; // Event with no payload
}>();
emit('change', 'hello'); // ✅
emit('update', 'world'); // ✅
emit('submit', '!'); // ❌ -> throws error in complie-time, no payload expected
emit('submit'); // ✅ -> no payload passed in, as expected
</script>
Enter fullscreen mode Exit fullscreen mode

See More

Legacy Features

Computed()

The computed wrapper getter function now accepts the previous value as a param to the getter function

const somethingComputed = computed( (previousValue) => somethingRef.value || previousValue );
Enter fullscreen mode Exit fullscreen mode
// previousValue can only be accessed in getter function

const somethingComputed = computed({
  get: (previousValue) => somethingRef.value || previousValue,
  set: (newValue) => {
      somethingRef.value = newValue;
  }
});
Enter fullscreen mode Exit fullscreen mode

Typescript

Example below demonstrates how computed can be type defined in Typescript

Below the computed getter will throw an error if it returns anything other then type ‘number’

const double = computed<number>(() => count.value * 2);

Watch()

Once

The watch now accepts a new new optional parameter called once

Implied by the name:

  • If once is true → watch callback function only executes once
  • If once is false → keeps usual behavior and watch callback function is called on eacvh reactive update

By default the value is false for this optional parameter

watch(() => something.value, () => {}, { once: false }); // 'once' false by default

watch(() => something.value, () => {}, { once: true }); // executes only once

Flush

The watch now accepts a new new optional parameter called flush

This option controls the timing of the callback function's execution relative to when the component is rendered.

Pre (default)

The callback runs before the component's render function is executed

// runs callback before DOM is mounted or updated
watch(() => something.value, () => {}, { flush: 'pre' }); // default value for 'flush'
Enter fullscreen mode Exit fullscreen mode
Post

The callback runs after the component's render function has executed and the DOM has been updated

// runs callback after DOM is mounted or updated
watch(() => something.value, () => {}, { flush: 'post' });
Enter fullscreen mode Exit fullscreen mode

New Macros

defineModel()

Simplifies the experience of 2-way binding in Vue with v-model

Type

function defineModel<T = any>( options?: object // see 'optional params' below ): Ref<T>;

v-model - Before vs Now

Before we would

  1. have to declare modelValue in props
  2. also updates it’s value via emit event
<!-- ChildComponent.vue -->
<!-- BEFORE -->
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
console.log(props.modelValue);
function onInput(e) {
  emit('update:modelValue', e.target.value);
} </script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
Enter fullscreen mode Exit fullscreen mode

Now prop registration and modelValue update event are handled by defineModel.

defineModel returns a ref value which can be directly accessed and updated

<!-- ChildComponent.vue -->
<!-- NOW -->
<script setup>
const modelValue = defineModel();
// optional params defineModel({});

function onInput(e) {
  modelValue.value = e.target.value;
}
</script>
<template>
  <input v-model="modelValue" />
</template>
Enter fullscreen mode Exit fullscreen mode

Optional Params

Option Name Description Example Usage
type Specifies the expected JavaScript type(s) for the prop (e.g., number, boolean, string). defineModel({ type: number })
required A boolean indicating if the parent component must provide a value via v-model. defineModel({ required: true })
default Sets a default value for the prop if the parent does not provide one. defineModel({ default: 'Hello' })
validator A function for custom prop validation. defineModel({ validator: (value) => value > 0 })
local When true, allows the ref to be mutated locally even if the parent didn't pass a corresponding v-model, essentially making the two-way binding optional. defineModel({ local: true, default: 'hi' })

Typescript

Type based declarations are supported by defineModel in typescript

<script setup lang="ts">
const modelValue = defineModel<string>() // ^? Ref<string | undefined>
// default model with options, required removes possible undefined values
const modelValue = defineModel<string>({ required: true });
</script>
Enter fullscreen mode Exit fullscreen mode

See more

There’s a lot more to unpack with this feature which are very well explained in the documents provided below

defineSlots() [Only supported on Typescript]

is used to define expected slots and their props expected slot props type

Type

function defineSlots<T = (Record<string, (props: any) => any>)>(): T;

Typescript

<script setup lang="ts">
defineSlots<{
  default?: (props: { msg: string }) => any;
  item?: (props: { id: number }) => any ;
 }>();
</script>
Enter fullscreen mode Exit fullscreen mode

It also returns the slots object, which is equivalent to the slots object exposed on the setup context or returned by useSlots()

See More

checkout more about limitations and details on this typescript based feature

New Features

useTemplateRefs()

returns reactive value of template Element referenced with the ref attribute

Type

function useTemplateRefs<T = HTMLElement>(key: string):Readonly<ShallowRef<T | null>>

Template Refs - Before vs Now

Before we would assign the ref template attribute the same name as the ref variable assigned to ref(null) in the setup context

<template>
 <div ref="myDiv">This is my element.</div>
</template>
<script setup>
import { ref } from 'vue';
const myDiv = ref(null);
console.log(myDiv.value);
</script>
Enter fullscreen mode Exit fullscreen mode

Now we use the same string assigned to the template ref attribute and pass it to the useTemplateRefs() api

<template>
  <div ref="myDiv">This is my element.</div>
</template>
<script setup>
import { useTemplateRef } from 'vue';
const myElementRef = useTemplateRef('myDiv'); // either null or html Element
console.log(myElementRef.value);
</script>
Enter fullscreen mode Exit fullscreen mode

Typescript

<template>
  <div ref="myDiv">This is my element.</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
const myElementRef = useTemplateRef<HTMLDivElement>('myDiv'); // or if the type can be inferred by @vue/language-tools, then type casting isn't necessary
</script>
Enter fullscreen mode Exit fullscreen mode

See More

useId()

Generates a unique ids that are stable across (don’t change) SSR and CSR (no risk of hydration mismatch).

These ids are crucial for correctly associating labels with form elements and other accessibility attributes.

⚠️ useId should never be called inside a computed wrapper

Type

function useId: string

Example

<template>
 <form>
   <label :for="myId">Name:</label>
   <input :id="myId" type="text" />
 </form>
</template>
<script setup>
import { useId } from 'vue';
const myId = useId(); // or if the type can be inferred by @vue/language-tools, then type casting isn't necessary
</script>
Enter fullscreen mode Exit fullscreen mode

See More

onWatcherCleanup()

Register a cleanup function to be executed when the current watcher (i.e. watch or watchEffect) is about to re-run (triggers).

Data Flow

Open image-20251106-163414.png

When to Use it

  • Cancel API requests
  • Clear timers
  • Remove event listeners
  • Free resources

Example

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

const controller = new FetchController()
const id = ref('');
const data = ref(null);

watch(() => id.value, (newId) => {
// note - this isn't best pratice, we shouldn't be awaiting async operations in watcher
// the purpose of this example is to show how onWatcherCleanup works             try {
 const response = await controller.fetch(newId);
 if (!response.ok) {
   throw new Error('data not found');
 }
 data.value = await response.json();
} catch {
 data.value = null
}
/*
- watcher cleanup registered - hoisted to the top of watcher callback and executes first before the try / catch blocks execute
*/

onWatcherCleanup(() => {
// terminates ongoing fetch calls, when the id updates (watcher is triggered)
  if(controller.isFetching) {
    controller.abort();
  }
 });
});
</script>
Enter fullscreen mode Exit fullscreen mode

See More

data-allow-mismatch

This attribute allows developers to ignore hydration mismatch console warnings.

When to Use it

Whenever template difference between server-side template and client-side template (after hydration) is expected (i.e. template that’s time sensitive)

Optional Params

The value can limit the allowed mismatch to a specific type. Allowed values are:

Option Name Description Example Usage
text Ignores mismatch warnings of HTML text <div data-allow-mismatch="text">Test</div>
children Ignores mismatch warnings of direct, nested children elements <div data-allow-mismatch="children">Test</div>
class Ignores mismatch warnings of Element class attribute <div data-allow-mismatch="class">Test</div>
style Ignores mismatch warnings of Element style attribute <div data-allow-mismatch="style">Test</div>
attribute Ignores mismatch warnings of general Element attribute mismatches <div data-allow-mismatch="attribute">Test</div>

⚠️ If no value is provided, all types of mismatches will be allowed.

Example

<template
 <div data-allow-mismatch>{{ time }}</div>
</template>
<script setup>
const time = new Date().getTime(); // value changes between ssr and csr
</script>
Enter fullscreen mode Exit fullscreen mode

See More

Shortened v-bind

Binding reactive values to a components (via v-bind) now works the same way as property assignment for Javascript Objects.

Shortened v-bind - Before vs Now

Before we would need to write out component v-bind assignments, even for values that share the same name in the setup context

<template>
 <TestComponent :test="test" />
</template>
<script setup>
import { ref } from 'vue';
const test = ref(null);
</script>
Enter fullscreen mode Exit fullscreen mode

Now v-bind attributes can be shortened to this when the they share the same name in the script context

<template>
 <TestComponent :test />
</template>
<script setup>
import { ref } from 'vue';
const test = ref(null);
</script>
Enter fullscreen mode Exit fullscreen mode

See More

Generic Component [Only supported on Typescript]

Vue components now support generic types on Typescript based Vue components. Generic types makes Vue components more flexible in terms of type requirements and reusable, allowing the component to work with props of different data types.

_ ⚠️ The data type observed for a particular generic must be consistent when passing in prop values, unless generics specified types are extended (ex._ generic="T extends string | number")

See Generic in ChildComponent.vue

<!-- ChildComponent.vue -->
<template>
 <p>{{ list.length }}</p>
</template>
<script setup lang="ts" generic="T, U">
defineProps<{
 list: T[], // ✅
 list2: U[], // ✅
 list3: U[], // ❌ -> throws error in complie-time, expects type 'U' is of type 'string'
}>();
</script>
Enter fullscreen mode Exit fullscreen mode
<!-- ParentComponent.vue -->
<template>
  <ChildComponent
    :list="[1,2,3]"
    :list2="['one', 'two', 'three']"
    :list3="[true, false]"
  >
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
</script>
Enter fullscreen mode Exit fullscreen mode

See More

Lazy Hydration Strategies

the Vue native api, defineAsyncComponent(), now includes an option to defer hydration (JS is executed on the the browser making the server-side template interactable).

Vue uses a number of hydration strategies, which allows developers to flexibly defer when parts of the server-side template becomes hydrated (JS execution of sever-side template is deferred), thus improving core web vital metrics, mainly INP (by deferring JS execution on the Main Thread during page load)

See the documentation here to explore the different hydration strategies developers can use to defer hydration in the code base

See More

Typescript Composition Api

Follow the documentation to see how Composition Api is integrated with Typescript here

Top comments (0)