Logo Craft Homelab Docs Контакты Telegram
Vue 3.5: что нового — Composition API и реактивность
Tue Dec 16 2025

Vue 3.5: улучшения реактивности и Composition API

Vue 3.5 — значительное обновление с улучшениями в реактивности и Composition API. Эта версия приносит оптимизации производительности, улучшенную поддержку TypeScript и новые возможности для организации кода.

Установка

Vue 3.5 устанавливается через npm или создаётся новый проект через CLI.

npm create vue@latest my-vue-app
# или
npm install vue@3.5

Команда create vue создаст проект с настроенным Vite и TypeScript.

Composition API

Composition API — это подход к организации кода, который позволяет группировать логику по функциональности, а не по опциям.

setup script

Синтаксис <script setup> — это компиляторная макросъёмка, которая упрощает использование Composition API.

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}

watch(count, (newVal, oldVal) => {
  console.log(`Changed from ${oldVal} to ${newVal}`)
})

onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <button @click="increment">
    Count: {{ count }}, Doubled: {{ doubled }}
  </button>
</template>

Реактивные объекты

reactive создаёт прокси-объект, который отслеживает изменения вложенных свойств. Для доступа к свойствам в template не нужно .value.

<script setup lang="ts">
import { reactive, toRefs } from 'vue'

const state = reactive({
  user: {
    name: 'John',
    email: 'john@example.com'
  },
  loading: false
})

const { user, loading } = toRefs(state)

function updateName(name: string) {
  state.user.name = name
}
</script>

<template>
  <div>
    <p>{{ user.name }}</p>
    <p>{{ loading }}</p>
  </div>
</template>

toRefs преобразует свойства reactive объекта в ref, сохраняя реактивность при деструктуризации.

provide/inject

provide/inject — механизм для передачи данных через дерево компонентов без пропсов. Полезно для глобальных зависимостей.

<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const count = ref(0)

provide('count', count)
provide('updateCount', (delta: number) => {
  count.value += delta
})
</script>

<!-- Child.vue -->
<script setup lang="ts">
import { inject } from 'vue'

const count = inject('count')
const updateCount = inject('updateCount') as (delta: number) => void
</script>

Pinia

Store definition

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref<{ name: string; email: string } | null>(null)
  const isLoggedIn = computed(() => user.value !== null)
  
  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    })
    user.value = await response.json()
  }
  
  function logout() {
    user.value = null
  }
  
  return { user, isLoggedIn, login, logout }
})

Использование в компоненте

<script setup lang="ts">
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

<template>
  <div v-if="userStore.isLoggedIn">
    Welcome, {{ userStore.user?.name }}
  </div>
  <div v-else>
    Please login
  </div>
</template>

Teleport

<template>
  <button @click="showModal = true">Open Modal</button>
  
  <Teleport to="body">
    <div v-if="showModal" class="modal-overlay" @click="showModal = false">
      <div class="modal" @click.stop>
        <h2>Modal Title</h2>
        <button @click="showModal = false">Close</button>
      </div>
    </div>
  </Teleport>
</template>

Suspense

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

Custom Directives

<script setup lang="ts">
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

Router

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('@/views/About.vue')
    },
    {
      path: '/user/:id',
      name: 'user',
      component: () => import('@/views/User.vue'),
      props: true
    }
  ]
})

export default router
router.beforeEach((to, from, next) => {
  const isAuthenticated = // check auth
  
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

TypeScript Support

<script setup lang="ts">
interface Props {
  title: string
  count?: number
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})

const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete'): void
}>()

function handleClick() {
  emit('update', props.count + 1)
}
</script>

Composables

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  
  return { x, y }
}
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  Mouse: {{ x }}, {{ y }}
</template>

Заключение

Vue 3.5 предоставляет мощный Composition API для создания реактивных приложений с отличной TypeScript поддержкой.