Vue Revisited II 2025
Introduction
The changes to Vue are so big I needed to give these changes their own page. First what I will be talking about.
- Composition API
- Data
- Methods
- LifeCycle hooks
- Directives
- Vue Router
- Lists, Teleport, Template Refs, next Tick
- Child Components
- Composables
A great site for tips with vue is vuejstips
Composition API
So I had forgotten how awful the options API is and since doing my stuff in composition API
Here is the old approach
Here is the new approach
The composables allows better code sharing than the mixins approach. This was done so well I just used it and it worked.
Data
Was much here aside from the reactive method for store non primative refs
<scripts>
const counterData = reactive({
counter:0,
title: 'My Counter'
})
</scripts>
Now you can use v-model to bind to it
<template>
<div>
<h4>Edit Counter Title:</h4>
<input v-model="counterData.title" type="text">
</div>
</template>
Methods
This is covering
- Methods
- Watches
- Computed
Methods
There was nothing to learn here.
Computed
This is the same a the signals in angular and basically just tracks when refs change and updates.
<template>
<div>
<p>First Name: {{ firstName }}</p>
<p>Last Name: {{ lastName }}</p>
<p>Full Name (computed): {{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Jane')
const lastName = ref('Doe')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
Watches
The watch approach is simple when dealing with ref. but when dealing with reactive it is a bit more complex but still easy pezzy
<script setup>
import { reactive, watch } from 'vue'
// Define a reactive object
const user = reactive({
firstName: 'Alice',
lastName: 'Smith'
})
// Watch a specific property using a getter
watch(
() => user.firstName,
(newVal, oldVal) => {
console.log(`First name changed from ${oldVal} to ${newVal}`)
}
)
// Watch the whole object deeply
watch(
() => ({ ...user }), // shallow copy to trigger on any change
(newVal, oldVal) => {
console.log('User object changed:', newVal)
},
{ deep: true }
)
</script>
LifeCycle hooks
So this allows us to do stuff an various stages of the lifecyle. These are the choices.
- Creation
- beforeCreate: Runs before data observation and event setup.
- created: Runs after data and methods are set up, but before DOM mounting.
- Mounting
- beforeMount: Called right before the component is mounted to the DOM.
- mounted: Called after the component is mounted and the DOM is available—great for DOM-dependent logic.
- Updating
- beforeUpdate: Called before the DOM is patched due to reactive data changes.
- updated: Called after the DOM has been patched.
- Unmounting
- beforeUnmount: Called right before the component is removed from the DOM.
- unmounted: Called after the component is removed—ideal for cleanup like removing event listeners or timers.
Other hooks are
- errorCaptured: Catches errors from child components.
- onActivated / onDeactivated: For <keep-alive> components—triggered when a component is activated or deactivated.
- renderTracked / renderTriggered: Useful for debugging reactivity.
Directives
So a directive is like v-show, v-if etc. And of course you can make you own.
Old way
<scripts>
{
{
...
directives: {
autofocus: {
mounted(el) {
el.focus()
}
}
}
}
}
</scripts>
<template>
<div>
<input v-model="counterData.title" type="text" v-autofocus>
</div>
</template>
In the new approach you need to create a directive in CamelCase
<scripts>
const vAutofocus = {
mounted(el) {
el.focus()
}
}
</scripts>
It did seem to be a bit silly to even mention this approach but I guess people like continuity. The right apporach is to make a directives directory and export it.
export const vAutofocus = {
mounted(el) {
el.focus()
}
}
</scripts>
Vue Router
So really the $route stuff all works in the templates but for the composition you need to use useRoute.
<scripts>
// Get the current route
const route = useRoute()
</scripts>
Now you can access what you need, e.g. route.path, route.params.id. For using the router we just use useRouter.
<scripts>
// Get the current router
const router = useRouter()
</scripts>
Now you can do router.push('/')
Lists, Template Refs, Next Tick, Teleport
Lists
Not sure why lists were on the list. There is not change really
<template>
<div
v-for="(menuItem, index) in menuItems"
:key="index"
class="pl-4 flex flex-row gap-2 items-center justify-start"
>
<div class="flex font-bold hover:text-headerHover underline">
{{ menuItem.title }}
</div>
</div>
</template>
Template Refs
So for refs this is referring to when you assign a ref to template element.
<template>
<div ref="parentRef" class="w-[35%] flex flex-col text-secondary gap-2">
<div
v-for="(menuItem, index) in menuItems"
:key="index"
:ref="(el) => setMenuItemRef(el as HTMLDivElement, index)"
:class="[
'w-full',
'flex',
'px-8',
'py-4',
index === selectedMenuIndex ? 'bg-[#2F2F2F]' : '',
]"
@click="handleMenuSelected(index)"
>
<Menu :menuItem="menuItem" :isActive="index === selectedMenuIndex" />
</div>
</div>
</template>
And in the script we have
<script setup lang="ts">
import { useScreenSizeDetector } from '@/composables/use-screen-size-detector'
import type { GeneralLinkItem } from '@/types/general-link-item'
import type { ISubMenus } from '@/types/menu-item'
import { ref, computed, watch } from 'vue'
import GeneralSmallMenu from '@/components/GeneralSmallMenu.vue'
import Menu from '@/components/TheMenu.vue'
import SubMenu from '@/components/SubMenu.vue'
defineOptions({
name: 'GeneralMenu',
})
<script setup lang="ts">
const props = defineProps<{
menuItems: ISubMenus[]
generalLinkItems: GeneralLinkItem[]
}>()
// Create refs to hold the DOM elements
const parentRef = ref<HTMLDivElement | null>(null)
const menuItemsRefs = ref<HTMLDivElement[]>([])
const selectedMenuIndex = ref(-1)
const activeMenuY = ref(0)
// Function to set refs for menu items
const setMenuItemRef = (el: HTMLDivElement | null, index: number) => {
if (el) {
menuItemsRefs.value[index] = el
}
}
// Watch for changes to get the active menu ref
watch(selectedMenuIndex, (newIndex) => {
const activeMenuRef = newIndex >= 0 ? menuItemsRefs.value[newIndex] : null
// Your menu position logic here
if (parentRef.value && activeMenuRef) {
const parentRect = parentRef.value.getBoundingClientRect()
const childRect = activeMenuRef.getBoundingClientRect()
const relativeY = childRect.bottom - parentRect.top
activeMenuY.value = relativeY
// Scroll into view if needed
activeMenuRef.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
})
</template>
Next Tick
This allows you to do something once the dom has updated. Here is an example
<template>
<div>
<button @click="showReply = true">Reply</button>
<input
v-if="showReply"
ref="replyInput"
placeholder="Type your reply..."
/>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const showReply = ref(false)
const replyInput = ref(null)
function focusInput() {
showReply.value = true
nextTick(() => {
replyInput.value?.focus()
})
}
</script>
Teleport
This allows you to reposition a move an element in the DOM. We do this to stop the influence of the parent components which may prevent it working properly.
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<p>This modal is teleported to the <code><body></code>!</p>
<button @click="showModal = false">Close</button>
</div>
</div>
</teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal {
background: white;
padding: 1rem;
margin: 10% auto;
width: 300px;
border-radius: 8px;
}
</style>