Composition API — script setup, ref e reactive
O <script setup> é a forma recomendada no Vue 3. Variáveis, computed e funções declaradas nele ficam disponíveis no template sem precisar de return explícito. Ele compila para código mais eficiente que o setup() manual e oferece melhor inferência TypeScript.
ref embrulha qualquer valor (primitivo ou objeto) em um container reativo com .value. reactive cria um objeto reativo profundo (Proxy) sem .value, mas perde reatividade ao ser desestruturado. Use ref como padrão — funciona em todos os casos. Use reactive apenas quando você gerencia um objeto com muitos campos relacionados e não precisa desestruturá-lo.
<script setup lang="ts">
import { ref, reactive, toRefs, toRef } from 'vue'
// ref — primitivos e objetos; acesso via .value no script
const count = ref(0)
const name = ref('Rafael')
const user = ref({ id: 1, name: 'Rafael', role: 'admin' })
count.value++
user.value.name = 'Ana' // reatividade funciona em objetos dentro de ref
// reactive — objeto profundo sem .value
const cart = reactive({
items: [] as { id: number; qty: number }[],
loading: false,
coupon: '',
})
cart.loading = true
// PROBLEMA — desestruturação quebra reatividade
const { loading } = cart // loading não é mais reativo!
// SOLUÇÃO 1 — toRefs converte cada propriedade em ref individual
const { loading: isLoading, items } = toRefs(cart)
isLoading.value = false // reatividade mantida
// SOLUÇÃO 2 — toRef para uma única propriedade
const cartItems = toRef(cart, 'items')
</script>No Vue 3.5+, props desestruturadas são reativas nativamente — sem precisar de toRefs:
<script setup lang="ts">
// Vue 3.5+: desestruturação reativa de props
const { title, count = 0 } = defineProps<{ title: string; count?: number }>()
// title e count são reativos — não precisam de .value no template
</script>computed — getter, getter+setter, computed com parâmetros
computed cria valores derivados com cache — só recalcula quando as dependências reativas mudam. Ao contrário de uma função chamada no template, computed não recalcula a cada re-render.
import { ref, computed } from 'vue'
// Getter simples — somente leitura
const firstName = ref('Rafael')
const lastName = ref('Marques')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Getter + Setter — permite atribuição
const fullNameRW = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const [f, ...rest] = val.split(' ')
firstName.value = f
lastName.value = rest.join(' ')
},
})
fullNameRW.value = 'Ana Costa' // dispara o setter
// computed com parâmetro via closure — retorna uma função
// útil para filtrar listas sem criar um estado temporário
const items = ref([
{ id: 1, category: 'fruit', name: 'Maçã' },
{ id: 2, category: 'veggie', name: 'Cenoura' },
{ id: 3, category: 'fruit', name: 'Banana' },
])
const byCategory = computed(() => (category: string) =>
items.value.filter(i => i.category === category)
)
// No template: {{ byCategory('fruit') }}
// Ou no script:
const fruits = computed(() => byCategory.value('fruit'))watch — immediate, deep, watchEffect, onWatcherCleanup, parar manualmente
watch observa uma dependência específica e recebe o valor anterior. Use quando você precisa reagir a uma mudança específica com lógica side-effect (fetch, timer, DOM externo). watchEffect re-executa automaticamente baseado em tudo que lê — mais conciso, mas sem acesso ao valor anterior.
import { ref, watch, watchEffect, onWatcherCleanup } from 'vue'
const search = ref('')
const userId = ref(1)
const profile = ref(null)
// watch simples
watch(search, (newVal, oldVal) => {
console.log(`Mudou de "${oldVal}" para "${newVal}"`)
})
// immediate: true — executa imediatamente ao montar
watch(userId, async (id) => {
profile.value = await fetch(`/api/users/${id}`).then(r => r.json())
}, { immediate: true })
// deep: true — observa mudanças profundas em objetos/arrays
const config = ref({ theme: 'dark', lang: 'pt' })
watch(config, (newConfig) => {
localStorage.setItem('config', JSON.stringify(newConfig))
}, { deep: true })
// Múltiplas fontes
watch([search, userId], ([newSearch, newId], [oldSearch, oldId]) => {
console.log('search ou userId mudou')
})
// watchEffect — rastreia dependências automaticamente
watchEffect(() => {
document.title = search.value
? `Resultados: ${search.value}`
: 'Início'
})
// onWatcherCleanup (Vue 3.5) — limpa side effects do watcher anterior
// chamado antes de cada re-execução e ao desmontar
watch(search, async (query) => {
const controller = new AbortController()
// registra função de cleanup — cancela fetch anterior
onWatcherCleanup(() => controller.abort())
const data = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
}).then(r => r.json()).catch(() => null)
if (data) results.value = data
})
// Parar watch manualmente — armazene o retorno
const stopWatch = watch(search, handler)
// ...mais tarde:
stopWatch() // para de observar
// watchEffect também retorna uma função stop
const stopEffect = watchEffect(() => { /* ... */ })
stopEffect()Template syntax — interpolação, v-bind, v-on com modificadores completos
O template Vue compila para chamadas eficientes de render function. Interpolação {{ }} escapa HTML automaticamente (sem XSS). v-html insere HTML bruto — use apenas com conteúdo confiável.
<template>
<!-- Interpolação — HTML escapado automaticamente -->
<p>{{ message }}</p>
<p>{{ price.toFixed(2) }}</p>
<!-- v-bind — vincula atributo dinamicamente (atalho :) -->
<img :src="user.avatar" :alt="user.name" />
<button :disabled="loading">Enviar</button>
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[baseClass, conditionalClass]"></div>
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<!-- v-bind shorthand (Vue 3.4+) — :id é equivalente a :id="id" -->
<div :id :class></div>
<!-- v-on — eventos (atalho @) -->
<!-- Modificadores de evento -->
<button @click.stop="handler">stop — para propagação</button>
<form @submit.prevent="submit">prevent — preventDefault</form>
<div @click.self="handler">self — só dispara no próprio elemento</div>
<button @click.once="handler">once — dispara uma única vez</button>
<div @click.capture="handler">capture — fase de captura</div>
<div @scroll.passive="onScroll">passive — não chama preventDefault (scroll suave)</div>
<!-- Modificadores de teclado -->
<input @keydown.enter="confirm" />
<input @keydown.escape="cancel" />
<input @keydown.tab="next" />
<input @keydown.ctrl.enter="submitForm" /> <!-- combinação -->
<input @keydown.meta.s="save" /> <!-- Cmd+S no Mac -->
<!-- Modificadores de mouse -->
<button @click.left="onLeft">esquerdo</button>
<button @click.right.prevent="onRight">direito (sem menu)</button>
<button @click.middle="onMiddle">meio</button>
<!-- Evento dinâmico -->
<div @[eventName]="handler"></div>
<!-- v-html — apenas conteúdo confiável! -->
<div v-html="trustedHtml"></div>
</template>v-if vs v-show — quando usar e performance
v-if remove e recria o elemento do DOM — tem custo maior na alternância, mas é mais eficiente quando o elemento raramente aparece. v-show apenas alterna display: none — mantém o elemento no DOM, tem custo maior na renderização inicial mas alternância barata.
<template>
<!-- v-if — melhor para condições que raramente mudam
ou quando o conteúdo é pesado e não precisa existir antecipadamente -->
<HeavyChart v-if="showChart" />
<!-- v-else-if e v-else — cadeia condicional -->
<div v-if="status === 'loading'">
<Spinner />
</div>
<div v-else-if="status === 'error'">
<ErrorMessage :message="errorMsg" />
</div>
<div v-else>
<DataTable :rows="rows" />
</div>
<!-- v-show — melhor para elementos que alternam frequentemente
(menus, modais, abas, sidebar toggle) -->
<Sidebar v-show="sidebarOpen" />
<Tooltip v-show="isHovered" :text="tooltipText" />
<!-- v-if em <template> — condicional sem wrapper extra no DOM -->
<template v-if="user.isAdmin">
<AdminBadge />
<AdminMenu />
</template>
</template>Regra prática: use v-show quando o elemento precisa aparecer/sumir repetidamente na interação do usuário (toggle de menu, tooltip, abas). Use v-if quando a condição é geralmente estável ou o componente é caro de inicializar.
v-for — índice, objeto, range, key obrigatória
v-for itera sobre arrays, objetos e ranges numéricos. A prop :key é obrigatória e deve ser um valor único e estável — permite ao Vue rastrear elementos para atualizações eficientes. Nunca use o índice como key em listas que mudam de ordem.
<template>
<!-- Array com índice -->
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
<!-- Objeto — (valor, chave, índice) -->
<div v-for="(value, key, index) in userObject" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
<!-- Range numérico — começa em 1 -->
<span v-for="n in 5" :key="n">{{ n }}</span>
<!-- v-for em <template> — sem wrapper extra -->
<template v-for="item in items" :key="item.id">
<dt>{{ item.term }}</dt>
<dd>{{ item.description }}</dd>
</template>
<!-- v-for com componente -->
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
</template>v-for com v-if — nunca no mesmo elemento. v-if tem precedência maior (Vue 3), então v-for ainda não foi processado quando v-if avalia. Use <template v-for> externo com v-if interno:
<!-- ERRADO — v-if não tem acesso a item -->
<li v-for="item in items" v-if="item.active" :key="item.id">
<!-- CORRETO — template externo, v-if avalia item disponível -->
<template v-for="item in items" :key="item.id">
<li v-if="item.active">{{ item.name }}</li>
</template>
<!-- OU filtre na computed -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>v-model — primitivos, múltiplos, transformação, defineModel (Vue 3.4)
v-model é açúcar sintático para :modelValue + @update:modelValue. Em elementos nativos, expande para o evento nativo correto (:value + @input para text, :checked + @change para checkbox).
<!-- Primitivos com modificadores -->
<input v-model="name" /> <!-- string -->
<input v-model.trim="name" /> <!-- remove espaços -->
<input v-model.number="age" type="number"/> <!-- converte para number -->
<input v-model.lazy="email" /> <!-- atualiza no blur/change -->
<input type="checkbox" v-model="checked" /> <!-- boolean -->
<select v-model="selected"> <!-- string ou array -->
<option v-for="o in options" :key="o.value" :value="o.value">
{{ o.label }}
</option>
</select>
<!-- Múltiplos v-model com nomes -->
<UserForm
v-model:firstName="form.firstName"
v-model:lastName="form.lastName"
v-model:email="form.email"
/>defineModel (Vue 3.4, estável) — forma recomendada para componentes com v-model:
<!-- Componente filho: RangeInput.vue -->
<script setup lang="ts">
// Declara modelValue e cria um ref que emite update:modelValue automaticamente
const value = defineModel<number>({ default: 0 })
// Com nome customizado: v-model:count
const count = defineModel<number>('count', { required: true })
// Com transformação — valida ou transforma na entrada/saída
const price = defineModel<number>('price', {
set(val) { return Math.max(0, val) }, // impede valores negativos
})
</script>
<template>
<input type="range" v-model="value" min="0" max="100" />
</template>
<!-- Pai -->
<RangeInput v-model="volume" v-model:count="itemCount" />Componentes — defineProps com TypeScript, withDefaults, runtime vs compile-time
Props definem a interface pública de entrada de um componente. Prefira validação por TypeScript (compile-time) para projetos TypeScript — é mais concisa e tem melhor DX. Use validação runtime quando precisar de validação complexa em runtime (funções validator).
<script setup lang="ts">
// Validação compile-time — TypeScript infere os tipos
const props = defineProps<{
title: string
count: number
status?: 'active' | 'inactive' | 'pending' // opcional
tags: string[]
author: { name: string; email: string }
}>()
// withDefaults — define valores padrão para props opcionais
const propsWithDefaults = withDefaults(defineProps<{
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
items?: string[]
}>(), {
size: 'md',
disabled: false,
items: () => [], // arrays/objetos precisam de factory function
})
// Validação runtime — mais flexível, roda no browser
defineProps({
price: {
type: Number,
required: true,
validator: (val: number) => val >= 0, // validação customizada
},
mode: {
type: String as PropType<'light' | 'dark'>,
default: 'light',
},
})
</script>Emits — defineEmits com TypeScript, validação, bubbling vs component events
Emits declaram o contrato de saída do componente. Eventos de componente não borbulham pelo DOM como eventos nativos — @click em um componente filho chama o handler apenas no pai imediato, não nos ancestrais.
<script setup lang="ts">
// TypeScript: array de tuplas [payload...] por evento
const emit = defineEmits<{
'update:modelValue': [value: string]
'item-selected': [item: { id: number; name: string }]
'form-submit': [data: FormData, isValid: boolean]
'cancel': [] // sem payload
}>()
// Validação runtime com função — retorna boolean
const emit = defineEmits({
'item-selected': (item: { id: number }) => {
if (!item.id) {
console.warn('item-selected requer um id válido')
return false
}
return true
},
})
function handleSelect(item: { id: number; name: string }) {
emit('item-selected', item)
}
// Componente filho emite — pai ouve com @item-selected
// Eventos NÃO borbulham para avô — diferente de eventos DOM nativos
</script>Slots — default, named, scoped, fallback content, slot com v-if
Slots permitem ao componente pai injetar conteúdo HTML arbitrário em pontos específicos do template filho. Scoped slots expõem dados do filho para o conteúdo injetado pelo pai.
<!-- DataTable.vue — define os slots -->
<template>
<div class="table-wrapper">
<!-- Slot default com fallback -->
<slot>
<p>Nenhum conteúdo fornecido.</p>
</slot>
<!-- Slot nomeado: header -->
<slot name="header" />
<!-- Slot nomeado: footer — condicional -->
<slot v-if="hasFooter" name="footer" />
<!-- Scoped slot: expõe dados do filho para o pai -->
<tbody>
<tr v-for="row in rows" :key="row.id">
<slot name="row" :row="row" :index="rows.indexOf(row)" />
</tr>
</tbody>
<!-- Slot com dados de loading -->
<slot name="loading" :progress="loadProgress">
<DefaultSpinner :progress="loadProgress" />
</slot>
</div>
</template>
<!-- Pai usando DataTable -->
<template>
<DataTable :rows="orders">
<!-- Slot default -->
Conteúdo extra aqui
<!-- Slot nomeado — atalho # -->
<template #header>
<h2>Pedidos Recentes</h2>
</template>
<!-- Scoped slot — desestrutura os dados expostos pelo filho -->
<template #row="{ row, index }">
<td>{{ index + 1 }}</td>
<td>{{ row.id }}</td>
<td :class="row.status">{{ row.total }}</td>
</template>
<!-- Slot nomeado com fallback override -->
<template #loading="{ progress }">
<CustomSpinner :value="progress" color="blue" />
</template>
</DataTable>
</template>defineExpose — expondo API de componentes para template refs
Por padrão, componentes com <script setup> são fechados — o pai não tem acesso a nada do filho via template ref. defineExpose define explicitamente o que pode ser acessado externamente.
<!-- Modal.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const isOpen = ref(false)
const lastAction = ref<string | null>(null)
function open() {
isOpen.value = true
}
function close() {
isOpen.value = false
lastAction.value = 'closed'
}
// Expõe apenas o necessário — API pública do componente
defineExpose({ open, close, isOpen })
// Não expõe: lastAction (detalhe interno)
</script>
<!-- Pai -->
<script setup lang="ts">
import { useTemplateRef } from 'vue' // Vue 3.5+
import Modal from './Modal.vue'
import type { ComponentExposed } from 'vue' // helpers de tipo
const modalRef = useTemplateRef('modal')
function showModal() {
modalRef.value?.open()
}
</script>
<template>
<Modal ref="modal" />
<button @click="showModal">Abrir modal</button>
</template>Template refs — useTemplateRef (Vue 3.5), ref em v-for, ref em componentes
Template refs dão acesso direto ao elemento DOM ou instância de componente. Vue 3.5 introduziu useTemplateRef() que amarra o ref ao atributo ref por nome, eliminando a dependência de nome de variável.
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
// Vue 3.5+ — useTemplateRef (recomendado)
const inputRef = useTemplateRef<HTMLInputElement>('searchInput')
// Vue 3.4 e anterior — variável com mesmo nome do atributo ref
const listRef = ref<HTMLUListElement | null>(null)
onMounted(() => {
inputRef.value?.focus() // foca o input ao montar
})
// ref em v-for — o ref se torna um array de elementos
const itemRefs = useTemplateRef<HTMLLIElement[]>('items')
// itemRefs.value é HTMLLIElement[]
</script>
<template>
<input ref="searchInput" type="search" />
<ul ref="list">
<li v-for="item in items" :key="item.id" ref="items">
{{ item.name }}
</li>
</ul>
</template>Lifecycle hooks completos
Os lifecycle hooks permitem executar código em momentos específicos do ciclo de vida do componente. Use onMounted para inicializar, onBeforeUnmount para limpeza.
import {
onBeforeMount, // antes do primeiro render (DOM não disponível)
onMounted, // após o primeiro render (DOM disponível)
onBeforeUpdate, // antes de cada re-render por mudança reativa
onUpdated, // após cada re-render
onBeforeUnmount, // antes de desmontar (ainda no DOM)
onUnmounted, // após desmontar (DOM removido)
onActivated, // ao entrar em KeepAlive
onDeactivated, // ao sair de KeepAlive
onErrorCaptured, // captura erros de descendentes
} from 'vue'
// Ordem de execução:
// Montagem: setup → onBeforeMount → render → onMounted
// Atualização: onBeforeUpdate → re-render → onUpdated
// Desmontagem: onBeforeUnmount → desmontagem → onUnmounted
onMounted(async () => {
// DOM disponível — buscar dados, inicializar libs externas, manipular DOM
items.value = await fetchItems()
chart = new Chart(canvasRef.value!, config)
})
onBeforeUnmount(() => {
// Limpar ANTES de desmontar — ainda pode acessar o DOM
chart.destroy()
clearInterval(pollInterval)
window.removeEventListener('resize', handleResize)
socket.disconnect()
})
onUpdated(() => {
// Cuidado: modificar estado aqui causa loop infinito
// Use para interagir com o DOM após atualização (scroll, third-party libs)
listEl.value?.scrollTo({ top: listEl.value.scrollHeight, behavior: 'smooth' })
})
onActivated(() => {
// Componente voltou a ser exibido pelo KeepAlive
// Ideal para retomar polls, timers ou atualizar dados stale
startPolling()
})
onDeactivated(() => {
// Componente foi cacheado pelo KeepAlive (não desmontado)
stopPolling()
})
onErrorCaptured((err, instance, info) => {
// Captura erros de qualquer descendente
// Retorne false para evitar que o erro propague para cima
logErrorToService(err, info)
return false
})Provide / Inject — símbolos como chave, default, readonly
provide/inject resolve o problema de prop drilling — passa dados de um ancestral para descendentes distantes sem passar por cada nível intermediário. Ideal para temas, configurações globais, contextos de formulário.
// types/injection-keys.ts — centralize as chaves como símbolos tipados
import type { InjectionKey, Ref } from 'vue'
export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')
export const UserKey: InjectionKey<{ id: number; name: string }> = Symbol('user')
// Componente ancestral — App.vue ou layout
import { provide, ref, readonly } from 'vue'
import { ThemeKey, UserKey } from '@/types/injection-keys'
const theme = ref<'light' | 'dark'>('dark')
// readonly — impede que descendentes modifiquem diretamente
provide(ThemeKey, readonly(theme))
provide(UserKey, { id: 1, name: 'Rafael' })
// Provê função de toggle — descendentes chamam isso para mudar
provide('toggleTheme', () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
})
// Componente descendente — qualquer nível abaixo
import { inject } from 'vue'
import { ThemeKey } from '@/types/injection-keys'
// Tipo inferido de InjectionKey — sem cast manual
const theme = inject(ThemeKey) // Ref<'light'|'dark'> | undefined
const themeWithDefault = inject(ThemeKey, ref('light')) // sempre definido
const toggle = inject<() => void>('toggleTheme')Composables — padrão, convenções, assíncronos, state compartilhado
Composables são funções que encapsulam e reutilizam lógica stateful usando a Composition API. Por convenção o nome começa com use. Devem ser chamados apenas no setup() ou <script setup>, não dentro de condicionais ou loops.
// composables/useLocalStorage.ts — state persistido no localStorage
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, initialValue: T) {
const stored = localStorage.getItem(key)
const value = ref<T>(stored ? JSON.parse(stored) : initialValue)
watch(value, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
}, { deep: true })
function clear() {
localStorage.removeItem(key)
value.value = initialValue
}
return { value, clear }
}
// composables/usePagination.ts — lógica de paginação reutilizável
import { ref, computed } from 'vue'
export function usePagination(totalItems: Ref<number>, perPage = 10) {
const currentPage = ref(1)
const totalPages = computed(() => Math.ceil(totalItems.value / perPage))
const offset = computed(() => (currentPage.value - 1) * perPage)
function goTo(page: number) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
return { currentPage, totalPages, offset, goTo }
}
// State compartilhado entre instâncias — singleton via module scope
// composables/useGlobalCart.ts
const cartItems = ref<CartItem[]>([]) // fora da função — compartilhado!
export function useGlobalCart() {
const total = computed(() =>
cartItems.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
function addItem(item: CartItem) {
const existing = cartItems.value.find(i => i.id === item.id)
if (existing) existing.qty++
else cartItems.value.push({ ...item, qty: 1 })
}
return { cartItems, total, addItem }
}Composables built-in do Vue — useTemplateRef, useId, useAttrs, useSlots, useCssModule
import {
useTemplateRef, // Vue 3.5 — ref tipado por nome
useId, // Vue 3.5 — ID único para acessibilidade
useAttrs, // atributos não declarados como props (fallthrough)
useSlots, // acesso programático aos slots
useCssModule, // acesso a CSS Modules no script
} from 'vue'
// useId — gera IDs únicos para label/input pairs (SSR-safe)
const labelId = useId() // ex: "v-0", "v-1"
// <label :for="labelId">Nome</label>
// <input :id="labelId" />
// useAttrs — atributos não declarados como props
// útil para componentes wrapper que repassam atributos para elemento interno
const attrs = useAttrs()
// attrs.class, attrs.style, attrs['data-testid'] etc.
// Por padrão, attrs caem no elemento raiz (inheritAttrs: true)
// useSlots — verificar se slot foi fornecido
const slots = useSlots()
const hasHeader = computed(() => !!slots.header)
// useCssModule — acesso ao CSS Module no script
const css = useCssModule()
// css.button, css.active etc.
// No template: :class="$style.button" ou :class="css.button"<!-- Exemplo: BaseInput.vue com useAttrs para wrapper transparente -->
<script setup lang="ts">
import { useAttrs, useId } from 'vue'
defineProps<{ label: string; modelValue: string }>()
defineEmits<{ 'update:modelValue': [string] }>()
defineOptions({ inheritAttrs: false }) // não aplica attrs no root
const attrs = useAttrs()
const inputId = useId()
</script>
<template>
<div class="field">
<label :for="inputId">{{ label }}</label>
<input
:id="inputId"
v-bind="attrs"
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</div>
</template>Pinia — defineStore, state, getters, actions, $patch, $reset, $subscribe, plugins
Pinia é o gerenciador de estado oficial do Vue 3, substituto do Vuex. Oferece TypeScript nativo, DevTools integrado, e suporte a SSR.
// stores/useCartStore.ts — estilo Options (semelhante ao Vuex)
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
coupon: null as string | null,
loading: false,
}),
getters: {
total: (state) => state.items.reduce((s, i) => s + i.price * i.qty, 0),
itemCount: (state) => state.items.reduce((s, i) => s + i.qty, 0),
// getter usando outro getter
totalWithDiscount(): number {
return this.coupon ? this.total * 0.9 : this.total
},
},
actions: {
addItem(product: Product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) {
existing.qty++
} else {
this.items.push({ ...product, qty: 1 })
}
},
async applyCoupon(code: string) {
this.loading = true
try {
const valid = await validateCoupon(code) // chamada API
if (valid) this.coupon = code
else throw new Error('Cupom inválido')
} finally {
this.loading = false
}
},
},
})
// stores/useUserStore.ts — estilo Setup (mais flexível, como composable)
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials: Credentials) {
user.value = await authService.login(credentials)
}
function logout() {
user.value = null
}
return { user, isLoggedIn, login, logout }
})
// Uso nos componentes
import { storeToRefs } from 'pinia'
const cart = useCartStore()
const { items, total } = storeToRefs(cart) // reatividade preservada
cart.addItem(product)
// $patch — múltiplas mutações em uma transação
cart.$patch({ coupon: 'SAVE10', loading: false })
cart.$patch((state) => {
state.items = state.items.filter(i => i.qty > 0)
})
// $reset — volta ao estado inicial (apenas Options style)
cart.$reset()
// $subscribe — observar mudanças no store
cart.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state.items))
})
// Plugin Pinia — adiciona funcionalidade a todos os stores
export function localStoragePlugin({ store }: { store: Store }) {
const saved = localStorage.getItem(store.$id)
if (saved) store.$patch(JSON.parse(saved))
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// main.ts
const pinia = createPinia()
pinia.use(localStoragePlugin)Vue Router — rotas aninhadas, params, guards, lazy loading
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'), // lazy
children: [
{
path: '', // path vazio = filho default do layout
name: 'home',
component: () => import('@/pages/HomePage.vue'),
},
{
path: 'orders',
component: () => import('@/pages/OrdersPage.vue'),
meta: { requiresAuth: true },
children: [
{
path: ':id', // /orders/123
name: 'order-detail',
component: () => import('@/pages/OrderDetail.vue'),
},
],
},
],
},
{
path: '/login',
name: 'login',
component: () => import('@/pages/LoginPage.vue'),
},
{
path: '/:pathMatch(.*)*', // 404 — catch-all
component: () => import('@/pages/NotFound.vue'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition
return { top: 0, behavior: 'smooth' }
},
})
// Guard global — autenticação
router.beforeEach(async (to, from) => {
if (to.meta.requiresAuth && !useAuthStore().isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
// Uso nos componentes
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Params e query
const orderId = route.params.id as string // /orders/123 → '123'
const page = route.query.page ?? '1' // /orders?page=2 → '2'
// Navegação programática
router.push({ name: 'order-detail', params: { id: '456' } })
router.push({ path: '/orders', query: { status: 'pending' } })
router.replace('/login')
router.back()
// Guards por componente — apenas com Options API ou no script
// (Vue Router 4 recomenda guards globais ou beforeEach com meta)
</script><!-- Guards in-component com Composition API -->
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// beforeRouteLeave — confirma saída se há mudanças não salvas
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = confirm('Deseja sair sem salvar?')
if (!answer) return false // cancela navegação
}
})
// beforeRouteUpdate — rota atualiza sem desmontar componente (mesmo componente, params diferentes)
onBeforeRouteUpdate(async (to) => {
orderId.value = to.params.id as string
await loadOrder(orderId.value)
})
</script>Transições e animações — Transition, TransitionGroup, CSS, JS hooks, GSAP
<!-- Transition — um único elemento/componente -->
<template>
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>
<!-- mode="out-in" — elemento antigo sai primeiro, novo entra depois -->
<!-- mode="in-out" — novo elemento entra primeiro -->
<!-- TransitionGroup — lista de elementos com animação de entrada/saída/movimento -->
<TransitionGroup name="list" tag="ul">
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</TransitionGroup>
</template>
<style>
/* Classes geradas por name="fade" */
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* TransitionGroup com movimento suave */
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-30px); }
.list-move { transition: transform 0.3s ease; }
.list-leave-active { position: absolute; } /* necessário para move */
</style>Integração com GSAP via JS hooks:
<script setup>
import gsap from 'gsap'
function onEnter(el: Element, done: () => void) {
gsap.from(el, {
duration: 0.5,
opacity: 0,
y: 40,
ease: 'power2.out',
onComplete: done,
})
}
function onLeave(el: Element, done: () => void) {
gsap.to(el, {
duration: 0.3,
opacity: 0,
y: -20,
onComplete: done,
})
}
</script>
<template>
<Transition :css="false" @enter="onEnter" @leave="onLeave">
<div v-if="show">Conteúdo animado</div>
</Transition>
</template>
<!-- :css="false" desativa as classes CSS — GSAP controla tudo -->KeepAlive — cache de componentes, include, exclude, max
<KeepAlive> mantém instâncias de componentes em memória ao invés de destruí-las. Útil para tabs, wizards ou listas com paginação onde você quer preservar o estado.
<template>
<!-- Mantém todos os componentes dinâmicos em cache -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
<!-- include — apenas componentes com esses nomes são cacheados -->
<KeepAlive include="ProductList,OrderHistory">
<component :is="activeView" />
</KeepAlive>
<!-- exclude — esses componentes NUNCA são cacheados -->
<KeepAlive :exclude="['LoginForm']">
<RouterView />
</KeepAlive>
<!-- max — limita o número de instâncias em cache (LRU) -->
<KeepAlive :max="5">
<component :is="currentTab" />
</KeepAlive>
<!-- Combinação com Suspense e Transition (ordem importa) -->
<RouterView v-slot="{ Component }">
<Transition name="slide" mode="out-in">
<KeepAlive :max="10">
<Suspense>
<component :is="Component" />
<template #fallback>Carregando...</template>
</Suspense>
</KeepAlive>
</Transition>
</RouterView>
</template>
<script setup>
import { onActivated, onDeactivated } from 'vue'
// Chamados ao entrar/sair do cache KeepAlive
onActivated(() => {
// Retomar polls, WebSocket, atualizar dados stale
refreshData()
})
onDeactivated(() => {
// Pausar recursos desnecessários
pauseVideoPlayer()
})
</script>Teleport — renderizar em outro lugar do DOM, defer (Vue 3.5)
<Teleport> renderiza conteúdo em outro nó do DOM enquanto mantém a lógica dentro do componente atual. Essencial para modais, tooltips e notificações que precisam escapar do contexto de stacking (z-index, overflow:hidden).
<template>
<!-- Renderiza no body — escapa overflow e z-index do pai -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay" @click.self="close">
<div class="modal">
<slot />
<button @click="close">Fechar</button>
</div>
</div>
</Teleport>
<!-- Teleport para elemento específico por seletor -->
<Teleport to="#notifications">
<Toast :message="toastMessage" />
</Teleport>
<!-- defer (Vue 3.5) — adia até que o target exista no DOM -->
<!-- Útil quando o target é renderizado DEPOIS do Teleport -->
<Teleport defer to="#portal-target">
<HeavyWidget />
</Teleport>
<!-- disabled — desabilita o teleport condicionalmente -->
<Teleport :disabled="isEmbedded" to="body">
<Modal />
</Teleport>
</template>Suspense — async setup, fallback, nested suspense
<Suspense> gerencia estados de carregamento de componentes com setup() assíncrono ou defineAsyncComponent. Ainda experimental, mas amplamente usado com Vue Router e Nuxt.
<!-- AsyncUserProfile.vue — setup assíncrono -->
<script setup lang="ts">
// await no top-level do setup — Suspense detecta e mostra fallback
const user = await fetchUser(userId)
const posts = await fetchUserPosts(userId)
</script>
<!-- Pai usando Suspense -->
<template>
<Suspense>
<!-- conteúdo principal — pode ter setup assíncrono -->
<template #default>
<AsyncUserProfile :userId="id" />
</template>
<!-- fallback — mostrado enquanto o filho resolve -->
<template #fallback>
<ProfileSkeleton />
</template>
</Suspense>
</template>
<script setup>
import { onErrorCaptured, ref } from 'vue'
// Captura erros do Suspense (rejeição de promises no setup)
const error = ref<Error | null>(null)
onErrorCaptured((err) => {
error.value = err
return false // impede propagação
})
</script>Diretivas customizadas
Diretivas customizadas permitem acesso direto ao DOM com hooks do ciclo de vida. Use quando você precisa de manipulação DOM que não cabe em um componente.
// directives/vFocus.ts — foca o elemento ao montar
import type { Directive } from 'vue'
export const vFocus: Directive<HTMLInputElement> = {
mounted(el, binding) {
// el — elemento DOM
// binding.value — valor passado: v-focus="true"
// binding.arg — argumento: v-focus:delay="300"
// binding.modifiers — modificadores: v-focus.lazy
if (binding.value !== false) el.focus()
},
}
// directives/vClickOutside.ts — detecta clique fora do elemento
export const vClickOutside: Directive = {
mounted(el, binding) {
el._clickOutsideHandler = (event: Event) => {
if (!el.contains(event.target as Node)) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutsideHandler)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutsideHandler)
},
}
// Registro global (main.ts)
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
// Registro local (apenas <script setup>)
// A variável deve começar com 'v' maiúsculo
const vHighlight: Directive = {
mounted: (el) => el.classList.add('highlight'),
}<template>
<input v-focus />
<div v-click-outside="closeMenu">Menu</div>
<p v-highlight>Texto destacado</p>
</template>Plugins — install function, propriedades globais, componentes globais
// plugins/i18n.ts — plugin de internacionalização simples
import type { App } from 'vue'
interface I18nOptions {
locale: string
messages: Record<string, Record<string, string>>
}
export default {
install(app: App, options: I18nOptions) {
// Propriedade global — acessível como $t() em templates
app.config.globalProperties.$t = (key: string) =>
options.messages[options.locale]?.[key] ?? key
// Composable disponível globalmente via provide
app.provide('i18n', {
locale: ref(options.locale),
t: (key: string) => options.messages[options.locale]?.[key] ?? key,
})
// Componente global — disponível sem import
app.component('TranslatedText', {
props: ['key'],
template: `<span>{{ $t(key) }}</span>`,
})
// Diretiva global
app.directive('t', {
mounted(el, binding) {
el.textContent = options.messages[options.locale]?.[binding.value] ?? binding.value
},
})
},
}
// main.ts
import i18n from '@/plugins/i18n'
app.use(i18n, {
locale: 'pt',
messages: {
pt: { hello: 'Olá', goodbye: 'Tchau' },
en: { hello: 'Hello', goodbye: 'Goodbye' },
},
})Renderização customizada — h(), render function, JSX
h() (hyperscript) cria vnodes diretamente — mais flexível que templates, mas menos legível. Use quando o template se torna muito complexo ou quando você precisa de geração programática de estrutura.
import { h, defineComponent, ref } from 'vue'
// Componente com render function
const DynamicList = defineComponent({
props: {
items: { type: Array as PropType<string[]>, required: true },
tag: { type: String, default: 'ul' },
},
setup(props) {
return () =>
h(props.tag, { class: 'dynamic-list' },
props.items.map((item, index) =>
h('li', { key: index, class: 'list-item' }, item)
)
)
},
})
// h() com componentes Vue
const app = h('div', { id: 'app' }, [
h(Header, { title: 'Meu App' }),
h(RouterView),
h(Footer),
])
// JSX em Vue — requer @vitejs/plugin-vue-jsx
const MyComponent = defineComponent({
setup() {
const count = ref(0)
return () => (
<div class="counter">
<p>Count: {count.value}</p>
<button onClick={() => count.value++}>+1</button>
</div>
)
},
})Performance — shallowRef, shallowReactive, markRaw, v-once, v-memo, defineAsyncComponent
import { shallowRef, shallowReactive, markRaw } from 'vue'
// shallowRef — reatividade apenas no nível raiz
// ideal para arrays/objetos grandes onde só você substitui o todo
const bigList = shallowRef<Item[]>([])
bigList.value = newItems // reativo — re-render
bigList.value.push(item) // NÃO reativo — Vue não detecta
// shallowReactive — apenas propriedades diretas são reativas
const state = shallowReactive({
count: 0, // reativo
nested: { x: 1 }, // NÃO reativo em mudanças profundas
})
// markRaw — objeto NUNCA será tornado reativo
// ideal para instâncias de third-party libs (charts, maps, editors)
const chart = markRaw(new Chart(canvas, config))
const mapInstance = markRaw(new MapLibreMap(options))
const state = reactive({ chart }) // chart não cria proxy<template>
<!-- v-once — renderiza uma vez e NUNCA atualiza -->
<header v-once>
<h1>{{ siteTitle }}</h1> <!-- conteúdo estático, sem re-render -->
</header>
<!-- v-memo — rerenderiza apenas se as deps na array mudaram -->
<!-- Similar ao React.memo para sub-árvores do template -->
<div v-for="item in hugelist" :key="item.id" v-memo="[item.selected]">
<!-- Re-renderiza APENAS quando item.selected mudar -->
<ExpensiveItemComponent :item="item" />
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// Lazy component — carrega o chunk JS apenas quando renderizado
const HeavyEditor = defineAsyncComponent(() =>
import('./components/HeavyEditor.vue')
)
// Com loading/error states e timeout
const AsyncTable = defineAsyncComponent({
loader: () => import('./components/DataTable.vue'),
loadingComponent: () => import('./components/Skeleton.vue'),
errorComponent: () => import('./components/ErrorState.vue'),
delay: 200, // aguarda 200ms antes de mostrar loading
timeout: 10000, // erro se não carregar em 10s
})
</script>SSR com Nuxt — páginas, composables, data fetching
Nuxt 3 é o meta-framework oficial para Vue com SSR. Usa convenções de arquivos para auto-import, roteamento e data fetching.
projeto-nuxt/
├── pages/ # /users.vue → /users (roteamento automático)
│ ├── index.vue
│ ├── users/
│ │ ├── index.vue # /users
│ │ └── [id].vue # /users/:id
├── components/ # auto-importados
├── composables/ # auto-importados — useX()
├── server/
│ └── api/ # /server/api/users.ts → /api/users
├── layouts/ # default.vue, auth.vue
└── nuxt.config.ts<!-- pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
// useFetch — fetch no SSR + hydration automática no cliente
// dados disponíveis antes do render (SEO-friendly)
const { data: user, pending, error, refresh } = await useFetch(
`/api/users/${route.params.id}`,
{
key: `user-${route.params.id}`, // cache key
transform: (data) => data.user,
}
)
// useAsyncData — wrapper mais flexível para lógica customizada
const { data: posts } = await useAsyncData(
'user-posts',
() => $fetch(`/api/users/${route.params.id}/posts`),
{ lazy: true } // não bloqueia navegação — carrega em background
)
// useLazyFetch — não bloqueia navegação; mostra pending state
const { data: recommendations, pending: loadingRecs } = useLazyFetch(
'/api/recommendations'
)
// definePageMeta — configura a página
definePageMeta({
layout: 'dashboard',
middleware: ['auth'],
})
</script>
<template>
<div v-if="pending">Carregando...</div>
<div v-else-if="error">Erro: {{ error.message }}</div>
<div v-else>
<h1>{{ user?.name }}</h1>
<button @click="refresh">Atualizar</button>
</div>
</template>Tabela de versões Vue 2 → 3.5
| Versão | Ano | Principais novidades |
|---|---|---|
| Vue 2 | 2016 | Options API (data, methods, computed); Vue.use; filters; $emit/$on; Vue.set para reatividade; Vuex; Object.defineProperty como base da reatividade |
| Vue 3.0 | 2020 | Composition API (setup(), ref, reactive); Fragment (múltiplos raízes); Teleport; Suspense experimental; reatividade baseada em Proxy; 40% menor; melhor tree-shaking; TypeScript nativo |
| Vue 3.2 | 2021 | <script setup> estável; defineProps/defineEmits com generics; v-bind em <style> (CSS reativo); defineExpose; performance de reatividade +55%; useSlots, useAttrs |
| Vue 3.3 | 2023 | defineProps com tipos importados (não apenas inline); defineModel experimental; defineSlots; generics em componentes (<script setup generic="T">); defineOptions; melhorias no @vue/language-tools |
| Vue 3.4 | 2024 | defineModel estável; reatividade reescrita (2× mais rápido); :id shorthand para :id="id"; erros de template em compile-time melhorados; v-bind com same-name shorthand |
| Vue 3.5 | 2024 | useTemplateRef(); useId(); onWatcherCleanup; props desestruturadas reativas nativamente; SSR streaming improvements; Teleport defer; useAttrs melhorado |