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.
- 1 Legacy Macros
- 1.1 defineProps()
- 1.2 withDefaults()
- 1.3 defineEmits()
- 2 Legacy Features
- 2.1 Computed()
- 2.2 Watch()
- 3 New Macros
- 3.1 defineModel()
- 3.2 defineSlots()
- 4 New Features
- 4.1 useTemplateRefs()
- 4.2 useId()
- 4.3 onWatcherCleanup()
- 4.4 data-allow-mismatch
- 4.5 Shortened v-bind
- 4.6 Generic Component
- 5 Lazy Hydration Strategies
- 6 Typescript Composition Api
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 }>()
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)
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>()
See More
- https://vuejs.org/guide/components/props#reactive-props-destructure
-
https://vuejs.org/guide/typescript/composition-api#typing-component-props [TS]
- See different patterns of defining props via
- Type-based props declarations (checks and validates compile time)
- Run-time props declaration
- Combining Type-based and Run-time Validation
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
}
);
See More
- https://vuejs.org/guide/typescript/composition-api#props-default-values [TS]
- https://vuejs.org/api/sfc-script-setup#default-props-values-when-using-type-declaration [TS]
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>
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>
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>
See More
- https://vuejs.org/api/sfc-script-setup#defineprops-defineemits [TS]
- https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits [TS]
- https://vuejs.org/guide/components/events [TS]
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 );
// previousValue can only be accessed in getter function
const somethingComputed = computed({
get: (previousValue) => somethingRef.value || previousValue,
set: (newValue) => {
somethingRef.value = newValue;
}
});
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'
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' });
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
- have to declare modelValue in props
- 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>
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>
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>
See more
There’s a lot more to unpack with this feature which are very well explained in the documents provided below
-
https://blog.vuejs.org/posts/vue-3-3#definemodel
- note, this feature is no longer experimental - it’s stable as of Vue 3.5
https://escuelavue.es/en/devtips/vue-3-modelvalue-definemodel-macro
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>
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
- https://blog.vuejs.org/posts/vue-3-3#typed-slots-with-defineslots [TS]
- https://vuejs.org/api/sfc-script-setup#defineslots [TS]
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>
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>
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>
See More
- https://vuejs.org/api/composition-api-helpers.html#usetemplateref
- https://blog.vuejs.org/posts/vue-3-5#usetemplateref
- https://vuejs.org/guide/typescript/composition-api#typing-template-refs [TS]
- https://vuejs.org/guide/typescript/composition-api#typing-component-template-refs [TS]
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>
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>
See More
- https://blog.vuejs.org/posts/vue-3-5#onwatchercleanup
- https://dev.to/alexanderop/vue-35s-onwatchercleanup-mastering-side-effect-management-in-vue-applications-9pn
- https://vuejs.org/api/reactivity-core.html#onwatchercleanup
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>
See More
- https://blog.vuejs.org/posts/vue-3-5#data-allow-mismatch
- https://vuejs.org/api/ssr.html#data-allow-mismatch
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>
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>
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>
<!-- 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>
See More
- https://vuejs.org/api/sfc-script-setup.html#generics
- https://blog.vuejs.org/posts/vue-3-3#generic-components
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
-
- Explains how lazy hydration via using the NUXT library
- As an abstract it explains, very well, how hydration works
- explains the benefits of using of lazy-hydration
Typescript Composition Api
Follow the documentation to see how Composition Api is integrated with Typescript here
Top comments (0)