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.

Ilustração da Composition API do Vue 3 com blocos de código reativo interconectados

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.

Pré-requisitos

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.

Counter.vuejavascript
<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.

UserProfile.vuejavascript
<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.

ProductList.vuejavascript
<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.

FullName.vuejavascript
<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.

When to use watch vs watchEffect?

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.

SearchComponent.vuejavascript
<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.

DeepWatch.vuejavascript
<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.

composables/useFetch.jsjavascript
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.

UserList.vuejavascript
<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.

composables/useForm.jsjavascript
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.

Important Cleanup

Sempre limpe os efeitos colaterais (timers, listeners de eventos, assinaturas) em onUnmounted para evitar vazamentos de memória.

LifecycleDemo.vuejavascript
<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.

InputFocus.vuejavascript
<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.

ChildComponent.vuejavascript
<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>
ParentComponent.vuejavascript
<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.

App.vue (or an ancestor component)javascript
<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>
DeepNestedComponent.vue (anywhere in the tree)javascript
<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

#vue 3
#composition api
#javascript
#frontend
#reactivity

Compartilhar

Artigos relacionados