Composition API do Vue 3: Guia completo para dominar a reatividade
Aprenda a utilizar a Composition API do Vue 3 com este guia prático. Domine ref, reactive, computed, watch e composables para construir aplicações Vue de alto desempenho.

A Composition API do Vue 3 representa uma evolução significativa na forma de estruturar componentes Vue. Essa abordagem organiza o código por funcionalidade em vez de por opções, facilitando a reutilização de lógica e a manutenção de aplicações complexas.
Este guia pressupõe conhecimentos básicos de Vue.js. Os exemplos utilizam a sintaxe <script setup> introduzida no Vue 3.2, que atualmente é a abordagem recomendada.
Understanding Reactivity with ref and reactive
A reatividade é o alicerce do Vue 3. Duas primitivas principais criam dados reativos: ref para valores primitivos e reactive para objetos complexos.
A função ref cria uma referência reativa que encapsula um valor. Para acessar ou modificar esse valor no script, é necessário utilizar a propriedade .value. No template, o Vue faz o desempacotamento automático dessa propriedade.
<script setup>
import { ref } from 'vue'
// Create a ref with initial value 0
const count = ref(0)
// Function that increments the counter
// Note: .value is required in the script
const increment = () => {
count.value++
}
// Reset function
const reset = () => {
count.value = 0
}
</script>
<template>
<!-- In the template, no .value needed -->
<div class="counter">
<p>Counter: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>Para objetos com múltiplas propriedades relacionadas, reactive oferece uma sintaxe mais natural, sem a necessidade de .value.
<script setup>
import { reactive } from 'vue'
// reactive for complex objects
// All properties are automatically reactive
const user = reactive({
name: 'Jane Smith',
email: 'jane@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})
// Direct property modification (no .value)
const updateTheme = (newTheme) => {
user.preferences.theme = newTheme
}
// Warning: do not reassign the entire object
// user = { ... } would break reactivity
</script>
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Theme: {{ user.preferences.theme }}</p>
</div>
</template>A regra geral: utilizar ref para valores primitivos (string, number, boolean) e reactive para objetos estruturados.
Computed Properties with computed
Propriedades computadas derivam valores a partir do estado reativo. São armazenadas em cache e só recalculam quando suas dependências mudam, o que as torna altamente performáticas.
<script setup>
import { ref, computed } from 'vue'
// Product list
const products = ref([
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 29, inStock: true },
{ id: 3, name: 'Keyboard', price: 79, inStock: false },
{ id: 4, name: 'Monitor', price: 299, inStock: true }
])
// Active filter
const showOnlyInStock = ref(false)
// computed: automatic filtering based on toggle
// Only recalculates if products or showOnlyInStock changes
const filteredProducts = computed(() => {
if (showOnlyInStock.value) {
return products.value.filter(p => p.inStock)
}
return products.value
})
// computed: total calculation with caching
const totalValue = computed(() => {
return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})
// computed: number of displayed items
const productCount = computed(() => filteredProducts.value.length)
</script>
<template>
<div>
<label>
<input type="checkbox" v-model="showOnlyInStock" />
Show only in-stock products
</label>
<p>{{ productCount }} products - Total: ${{ totalValue }}</p>
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>Propriedades computadas também podem ser de escrita, utilizando um getter e um setter. Isso é útil para transformações bidirecionais.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed with getter AND setter
const fullName = computed({
// Read: combine first and last name
get() {
return `${firstName.value} ${lastName.value}`
},
// Write: split the string into first/last name
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts.slice(1).join(' ') || ''
}
})
</script>
<template>
<!-- Modifying fullName updates firstName and lastName -->
<input v-model="fullName" placeholder="Full name" />
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</template>Watchers with watch and watchEffect
Os watchers executam efeitos colaterais em resposta a mudanças nos dados. O Vue 3 oferece duas abordagens: watch para controle preciso e watchEffect para rastreamento automático.
watch oferece controle preciso sobre as dependências e fornece tanto o valor anterior quanto o novo. watchEffect é mais simples quando todas as dependências reativas utilizadas devem disparar o efeito.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
// watch: observes a specific source
// Provides oldValue and newValue, allows debouncing
watch(searchQuery, async (newQuery, oldQuery) => {
console.log(`Search changed from "${oldQuery}" to "${newQuery}"`)
// Don't search if less than 3 characters
if (newQuery.length < 3) {
searchResults.value = []
return
}
isLoading.value = true
try {
// Simulated API call
const response = await fetch(`/api/search?q=${newQuery}`)
searchResults.value = await response.json()
} finally {
isLoading.value = false
}
}, {
// Watcher options
debounce: 300, // Wait 300ms after last keystroke
immediate: false // Don't execute immediately
})
// watchEffect: automatic dependency tracking
// Runs immediately and on every change
watchEffect(() => {
// All refs used here are automatically tracked
console.log(`Current state: ${searchResults.value.length} results`)
console.log(`Loading: ${isLoading.value}`)
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<p v-if="isLoading">Loading...</p>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
</ul>
</div>
</template>Para observar objetos aninhados ou arrays, a opção deep é necessária com watch.
<script setup>
import { reactive, watch } from 'vue'
const settings = reactive({
display: {
theme: 'light',
fontSize: 14
},
notifications: {
email: true,
push: false
}
})
// deep: true to observe nested changes
watch(
() => settings.display,
(newDisplay) => {
console.log('Display settings changed:', newDisplay)
// Save to localStorage for example
localStorage.setItem('display', JSON.stringify(newDisplay))
},
{ deep: true }
)
</script>Pronto para mandar bem nas entrevistas de Vue.js / Nuxt.js?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Creating Reusable Composables
Composables são funções que encapsulam lógica reativa reutilizável. Essa abordagem é um dos grandes pontos fortes da Composition API para compartilhar código entre componentes.
import { ref, watchEffect, toValue } from 'vue'
// Composable for HTTP requests
// Automatically handles loading, errors, and refetching
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
// Reusable fetch function
async function fetchData() {
isLoading.value = true
error.value = null
try {
// toValue() allows accepting a ref or a value
const response = await fetch(toValue(url))
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
// watchEffect for automatic refetch if url is a ref
watchEffect(() => {
fetchData()
})
// Expose state and methods
return {
data,
error,
isLoading,
refetch: fetchData
}
}Esse composable pode ser utilizado de forma declarativa em qualquer componente.
<script setup>
import { useFetch } from '@/composables/useFetch'
// Simple composable usage
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div>
<button @click="refetch" :disabled="isLoading">
Refresh
</button>
<p v-if="isLoading">Loading users...</p>
<p v-else-if="error" class="error">Error: {{ error }}</p>
<ul v-else-if="users">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>A seguir, outro exemplo de composable para gerenciar o estado de um formulário com validação.
import { reactive, computed } from 'vue'
// Form management composable
export function useForm(initialValues, validationRules) {
// Form state
const form = reactive({
values: { ...initialValues },
errors: {},
touched: {}
})
// Validate a specific field
const validateField = (field) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule(form.values[field])
if (result !== true) {
form.errors[field] = result
return false
}
}
form.errors[field] = null
return true
}
// Validate entire form
const validate = () => {
let isValid = true
for (const field in validationRules) {
if (!validateField(field)) {
isValid = false
}
}
return isValid
}
// computed: form is valid if no errors
const isValid = computed(() => {
return Object.values(form.errors).every(e => !e)
})
// Mark a field as touched (to display errors)
const touch = (field) => {
form.touched[field] = true
validateField(field)
}
// Reset the form
const reset = () => {
form.values = { ...initialValues }
form.errors = {}
form.touched = {}
}
return {
form,
isValid,
validate,
validateField,
touch,
reset
}
}Lifecycle Hook Management
A Composition API disponibiliza hooks de ciclo de vida como funções chamadas dentro do setup. Esses hooks permitem executar código em momentos específicos do ciclo de vida do componente.
Sempre limpe os efeitos colaterais (timers, listeners de eventos, assinaturas) em onUnmounted para evitar vazamentos de memória.
<script setup>
import {
ref,
onMounted,
onUnmounted,
onBeforeUpdate,
onUpdated
} from 'vue'
const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)
// Handler for resize
const handleResize = () => {
windowWidth.value = window.innerWidth
}
// onMounted: component inserted into DOM
// Ideal for initial API calls and event listeners
onMounted(() => {
console.log('Component mounted')
window.addEventListener('resize', handleResize)
// Example: initialize a third-party library
// chart = new Chart(chartRef.value, config)
})
// onUnmounted: component removed from DOM
// CRITICAL: clean up all side effects
onUnmounted(() => {
console.log('Component unmounted')
window.removeEventListener('resize', handleResize)
// Clean up subscriptions, timers, etc.
// chart?.destroy()
})
// onBeforeUpdate: before DOM update
onBeforeUpdate(() => {
console.log('Before update')
})
// onUpdated: after DOM update
onUpdated(() => {
updateCount.value++
console.log(`DOM updated (${updateCount.value} times)`)
})
</script>
<template>
<div>
<p>Window width: {{ windowWidth }}px</p>
<p>DOM updates: {{ updateCount }}</p>
</div>
</template>Template Refs and DOM Access
Os template refs fornecem acesso direto a elementos do DOM ou instâncias de componentes filhos. Isso continua sendo útil em casos onde a manipulação direta é necessária.
<script setup>
import { ref, onMounted } from 'vue'
// ref for the DOM element
// Same name as the ref attribute in the template
const inputRef = ref(null)
// Function to focus the input
const focusInput = () => {
// Access to the native DOM element
inputRef.value?.focus()
}
// Auto-focus on mount
onMounted(() => {
focusInput()
})
// Expose the method to parent if needed
defineExpose({
focus: focusInput
})
</script>
<template>
<div>
<!-- The ref attribute binds the element to inputRef -->
<input ref="inputRef" type="text" placeholder="Auto-focused" />
<button @click="focusInput">Refocus</button>
</div>
</template>Parent-Child Communication with Props and Emits
A Composition API moderniza a declaração de props e eventos com defineProps e defineEmits, oferecendo uma integração superior com TypeScript.
<script setup>
// defineProps: declares expected props
// With default values via withDefaults
const props = withDefaults(defineProps<{
title: string
count?: number
items?: string[]
}>(), {
count: 0,
items: () => []
})
// defineEmits: declares emitted events
// Precise payload typing
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'submit', data: { title: string; items: string[] }): void
}>()
// Function that emits an event
const handleSubmit = () => {
emit('submit', {
title: props.title,
items: props.items
})
}
const increment = () => {
emit('update', props.count + 1)
}
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="handleSubmit">Submit</button>
</div>
</template><script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])
// Handler for update event
const onUpdate = (newValue) => {
currentCount.value = newValue
}
// Handler for submit event
const onSubmit = (data) => {
console.log('Data submitted:', data)
}
</script>
<template>
<ChildComponent
title="My Component"
:count="currentCount"
:items="items"
@update="onUpdate"
@submit="onSubmit"
/>
</template>Pronto para mandar bem nas entrevistas de Vue.js / Nuxt.js?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Provide and Inject for Dependency Injection
Para compartilhar dados entre componentes distantes sem a necessidade de passar props por cada nível, o Vue 3 oferece provide e inject.
<script setup>
import { provide, ref, readonly } from 'vue'
// Global theme state
const theme = ref('light')
// Function to toggle theme
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide: makes available to descendants
// readonly prevents direct modification by children
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
<template>
<div :class="theme">
<slot />
</div>
</template><script setup>
import { inject } from 'vue'
// inject: retrieves value provided by an ancestor
// Default value if not found
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>
<template>
<div>
<p>Current theme: {{ theme }}</p>
<button @click="toggleTheme">
Switch to {{ theme === 'light' ? 'dark' : 'light' }}
</button>
</div>
</template>Conclusion
A Composition API do Vue 3 oferece uma abordagem poderosa e flexível para organizar o código de aplicações Vue. Conceitos-chave a reter:
- ref para valores primitivos, reactive para objetos complexos
- computed para valores derivados com cache automático
- watch e watchEffect para efeitos colaterais reativos
- Composables para extrair e reutilizar lógica entre componentes
- Hooks de ciclo de vida funcionais (
onMounted,onUnmounted, etc.) - provide/inject para injeção de dependências sem prop drilling
Essa abordagem facilita a criação de aplicações sustentáveis e testáveis, ao mesmo tempo em que oferece excelente integração com TypeScript. O próximo passo envolve explorar padrões avançados como composables assíncronos e a integração com Pinia para gerenciamento de estado global.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
