Options API vs Composition API no Vue 3: diferenças, exemplos e quando usar cada uma
16 de abril de 2026 às 21:30 - Por Larissa Santos

Options API vs Composition API no Vue.js: qual usar e quando?
Se você trabalha com Vue.js há algum tempo, com certeza já se deparou com essa dúvida, ou pelo menos ouviu alguém debater sobre ela. Afinal, o Vue 3 trouxe uma nova forma de escrever componentes, e desde então a pergunta não sai do radar da comunidade: Options API ou Composition API?
A boa notícia é que não existe uma resposta única e definitiva. A escolha depende do contexto, do projeto e, também, do seu gosto pessoal. Mas para fazer uma escolha consciente, você precisa entender bem as duas abordagens: o que cada uma oferece, como cada uma pensa a organização do código e onde cada uma começa a mostrar suas limitações.
É exatamente isso que vamos explorar aqui, de forma aprofundada e com exemplos práticos.
Um pouco de contexto: como chegamos até aqui
Até o Vue 2, existia basicamente uma única forma de estruturar um componente, o que hoje chamamos de Options API. Era o jeito Vue de fazer as coisas, e funcionava bem. Mas à medida que projetos cresciam em complexidade e a comunidade de desenvolvedores foi amadurecendo, algumas limitações foram ficando mais evidentes.
Em 2020, o Vue 3 foi lançado trazendo a Composition API como nova alternativa, inspirada em parte pelos React Hooks, mas com uma abordagem própria e mais alinhada ao modelo de reatividade do Vue. Desde então, as duas coexistem e a documentação oficial deixa claro que nenhuma será descontinuada.
O que é a Options API?
A Options API é a forma clássica de criar componentes no Vue. Você exporta um objeto com "opções" bem definidas, cada uma com sua responsabilidade:
data: onde vivem os dados reativos do componentemethods: onde ficam as funções que manipulam o estado ou respondem a eventoscomputed: propriedades derivadas, calculadas com base em dados reativoswatch: observadores que reagem a mudanças em valores específicospropseemits: para comunicação com componentes pai/filho- Lifecycle hooks como
mounted,created,updated,unmounted, entre outros
A estrutura é clara e previsível. Você sabe exatamente onde procurar cada coisa. Quer ver os dados? Vai em data. Quer ver os métodos? Vai em methods. Isso torna a curva de entrada bastante suave, especialmente para quem está começando.
Exemplo prático com Options API
Imagine um componente simples de contador com uma mensagem derivada:
<template>
<div>
<p>Contagem: {{ count }}</p>
<p>{{ message }}</p>
<button @click="increment">Incrementar</button>
<button @click="reset">Resetar</button>
</div>
</template>
<script>
export default {
name: 'Counter',
data() {
return {
count: 0
}
},
computed: {
message() {
return this.count === 0
? 'Nenhum clique ainda.'
: `Você clicou ${this.count} vez(es).`
}
},
methods: {
increment() {
this.count++
},
reset() {
this.count = 0
}
},
mounted() {
console.log('Componente montado com count:', this.count)
}
}
</script>
Repare que toda a lógica fica organizada dentro de "gavetas" separadas. Para quem vem do Vue 2 ou está aprendendo o framework agora, essa estrutura é bastante intuitiva. O problema começa quando o componente cresce.
O problema de escala na Options API
Imagine agora que esse mesmo componente precisa, além do contador, gerenciar também um formulário de contato, um estado de loading para uma requisição HTTP e uma lógica de validação. Com a Options API, o código de cada uma dessas responsabilidades vai estar fragmentado: parte em data, parte em methods, parte em computed, outra parte nos lifecycle hooks.
Você passa a ter um arquivo com 300 linhas onde, para entender uma única funcionalidade, precisa ficar rolando o componente de cima para baixo, saltando entre as seções. A documentação oficial do Vue ilustra esse problema com um diagrama que mostra exatamente como os blocos de código relacionados ficam espalhados quando usamos Options API em componentes complexos.
Outro ponto crítico é o reuso de lógica. Na Options API, a solução tradicional para compartilhar comportamento entre componentes eram os mixins, que trazem uma série de problemas:
- Colisão de nomes: se dois mixins definem uma propriedade com o mesmo nome, há conflito
- Origem obscura: ao olhar para
this.algumDado, é difícil saber se ele veio do próprio componente, de um mixin A ou de um mixin B - Acoplamento implícito: mixins podem depender de propriedades do componente host sem deixar isso claro na assinatura
Esses problemas ficam sérios em bases de código grandes, onde múltiplos mixins são combinados no mesmo componente.
O que é a Composition API?
A Composition API é uma alternativa que permite escrever a lógica do componente usando funções importadas diretamente da API do Vue, em vez de declarar um objeto de opções. O nome "composição" vem exatamente da ideia de compor o comportamento do componente a partir de funções menores e reutilizáveis.
Ela é usada principalmente junto com a sintaxe <script setup>, que é um "açúcar sintático" (syntactic sugar) que reduz o boilerplate necessário. Com <script setup>, tudo que você declara no script fica automaticamente disponível no template, sem precisar retornar explicitamente nada.
As principais funções da Composition API incluem:
ref()ereactive(): para criar estado reativocomputed(): para criar propriedades computadaswatch()ewatchEffect(): para observar mudanças reativasonMounted(),onUnmounted(),onUpdated()e outros hooks de ciclo de vidaprovide()einject(): para injeção de dependências entre componentes
Exemplo prático com Composition API
O mesmo componente de contador, reescrito com Composition API e <script setup>:
<template>
<div>
<p>Contagem: {{ count }}</p>
<p>{{ message }}</p>
<button @click="increment">Incrementar</button>
<button @click="reset">Resetar</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const message = computed(() => {
return count.value === 0
? 'Nenhum clique ainda.'
: `Você clicou ${count.value} vez(es).`
})
function increment() {
count.value++
}
function reset() {
count.value = 0
}
onMounted(() => {
console.log('Componente montado com count:', count.value)
})
</script>
À primeira vista, o código parece muito similar. E de fato é: para componentes simples, a diferença não é tão gritante. A grande vantagem começa a aparecer quando o componente cresce ou quando você precisa reutilizar lógica.
Uma diferença importante: ref e .value
Uma das primeiras dúvidas de quem migra para a Composition API é: por que precisamos de .value para acessar o dado?
A razão é técnica. ref() retorna um objeto reativo que encapsula o valor. Isso permite que o Vue rastreie as dependências corretamente. Dentro do <script setup>, você acessa e modifica o valor via .value. Já no template, o Vue faz o "unwrap" automaticamente. Você escreve {{ count }} e não {{ count.value }}.
const count = ref(0)
count.value++ // dentro do script: usa .value
// {{ count }} // no template: sem .value, Vue desempacota automaticamente
Para objetos e arrays, você pode usar reactive() em vez de ref(). Nesse caso, não há .value, pois o próprio objeto é reativo:
const state = reactive({
count: 0,
name: 'Vue'
})
state.count++ // sem .value
state.name = 'Nuxt'
A escolha entre ref e reactive é uma questão de preferência e contexto. Muitos desenvolvedores usam ref para tudo por consistência, enquanto outros preferem reactive para agrupar estados relacionados.
Comparando lado a lado: os lifecycle hooks
Uma das diferenças mais práticas entre as duas abordagens é como lidar com os hooks de ciclo de vida. Na Options API, eles são métodos do objeto de opções. Na Composition API, são funções importadas.
| Options API | Composition API |
|---|---|
created | (executa no setup) |
mounted | onMounted |
updated | onUpdated |
unmounted | onUnmounted |
beforeMount | onBeforeMount |
beforeUpdate | onBeforeUpdate |
beforeUnmount | onBeforeUnmount |
Vale destacar um comportamento interessante: na Composition API, você pode chamar o mesmo hook várias vezes dentro do mesmo componente, e todas as chamadas serão executadas. Na Options API, só há um mounted, um updated, etc.
onMounted(() => {
console.log('Inicializando o mapa...')
})
onMounted(() => {
console.log('Buscando dados iniciais...')
})
Isso pode parecer estranho no começo, mas faz muito sentido quando você começa a extrair lógica para composables, pois cada um pode registrar seus próprios hooks sem interferir nos outros.
O grande diferencial: os Composables
Se a Composition API tem um superpoder, esse superpoder tem nome: composables. São funções que encapsulam lógica com estado e podem ser reutilizadas em qualquer componente, sem os problemas dos mixins.
Por convenção, composables começam com use (assim como os hooks do React). Veja um exemplo real de um composable para rastrear a posição do mouse:
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
E agora, para usar em qualquer componente:
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Posição do mouse: {{ x }}, {{ y }}</p>
</template>
Repare o que acontece aqui: a lógica de adicionar e remover o event listener está completamente encapsulada no composable. O componente não precisa saber como isso funciona internamente, ele só consome o resultado. Não há risco de colisão de nomes, não há ambiguidade sobre a origem dos dados, e o composable pode ser testado de forma independente.
Isso seria muito mais difícil de alcançar com mixins na Options API.
TypeScript: onde a Composition API brilha mais
Com o crescimento do TypeScript no ecossistema frontend, essa é uma dimensão cada vez mais relevante na escolha entre as duas APIs.
A Options API foi projetada antes do TypeScript se tornar popular, e isso cria fricção. O sistema de tipos precisa "adivinhar" o que está disponível em this, o que exige malabarismos internos no Vue. Para mixins, a situação piora ainda mais, pois o TypeScript não consegue inferir de onde vêm as propriedades.
A Composition API, por outro lado, trabalha com variáveis e funções comuns do JavaScript, algo que o TypeScript entende naturalmente. A inferência de tipos funciona quase que automaticamente:
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref(0) // TypeScript infere: Ref<number>
const double = computed(() => count.value * 2) // infere: ComputedRef<number>
function increment(): void {
count.value++
}
</script>
Para projetos que usam TypeScript, a Composition API é claramente a abordagem mais ergonômica. Com a Options API e TypeScript, é necessário usar defineComponent e ainda assim algumas situações de tipagem ficam trabalhosas.
Performance e bundle size
Outro ponto que a documentação oficial destaca é que código escrito com <script setup> é mais eficiente na fase de minificação. Isso acontece porque, diferente da Options API onde as propriedades são acessadas via this, no <script setup> tudo é declarado no escopo local, e os minificadores conseguem renomear variáveis locais de forma muito mais agressiva.
O resultado prático é um bundle final um pouco menor, o que impacta positivamente o tempo de carregamento da aplicação, especialmente relevante para projetos grandes.
Além disso, se você usar exclusivamente a Composition API em um projeto, é possível configurar uma flag de compilação que remove o código da Options API do bundle do Vue em si, economizando alguns kilobytes adicionais.
Podem ser usadas juntas?
Sim! Uma dúvida comum é se é possível misturar as duas abordagens. A resposta é sim: você pode usar setup() como uma opção dentro de um componente Options API:
<script>
import { ref } from 'vue'
export default {
data() {
return {
legacyData: 'sou da Options API'
}
},
setup() {
const newData = ref('sou da Composition API')
return { newData }
}
}
</script>
Isso é especialmente útil durante migrações graduais, pois você pode ir introduzindo a Composition API em componentes existentes sem precisar reescrever tudo de uma vez.
O que não é recomendado é usar <script setup> (a sintaxe mais moderna) junto com opções tradicionais como data() ou methods no mesmo componente. Nesse caso, você deve escolher um ou outro.
Quando usar cada uma?
Depois de tudo isso, a pergunta inevitável: quando optar por cada abordagem?
Options API faz sentido quando:
- Você está iniciando no Vue e quer uma curva de aprendizado menor
- O projeto é pequeno ou de baixa complexidade
- A equipe tem familiaridade com Vue 2 e a migração precisa ser gradual
- Componentes têm responsabilidades bem delimitadas e não crescem muito
Composition API faz sentido quando:
- O projeto é de médio a grande porte
- Você usa TypeScript (ou pretende usar)
- Há lógica que precisa ser reutilizada entre múltiplos componentes
- A equipe preza por testes unitários, já que composables são muito mais fáceis de testar isoladamente
- Você está construindo uma biblioteca de componentes ou um design system
Na prática: projetos novos iniciados com Vue 3 dificilmente têm motivo para escolher a Options API como padrão. A Composition API com <script setup> é a direção clara do framework: é o que a documentação recomenda, é o que o ecossistema (Nuxt 3, Pinia, VueUse) adota nativamente.
Um exemplo real de organização com Composition API
Para fechar, um exemplo mais próximo do mundo real: um componente que busca uma lista de usuários de uma API.
<template>
<div>
<p v-if="loading">Carregando...</p>
<p v-if="error">Erro: {{ error }}</p>
<ul v-if="!loading && !error">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
users.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(fetchUsers)
</script>
Agora imagine que essa lógica de fetch (loading, error, dados) precisa ser reutilizada em vários lugares. Com a Composition API, você extrai para um composable:
// useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function execute() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return { data, loading, error, execute }
}
E o componente fica limpo e expressivo:
<script setup>
import { onMounted } from 'vue'
import { useFetch } from './composables/useFetch'
const {
data: users,
loading,
error,
execute
} = useFetch('https://jsonplaceholder.typicode.com/users')
onMounted(execute)
</script>
Conclusão
A Options API e a Composition API não são inimigas: são ferramentas diferentes, cada uma com seu contexto ideal. A Options API continua sendo uma escolha válida e não vai desaparecer. Mas a Composition API representa a evolução natural do Vue, trazendo mais flexibilidade, melhor suporte a TypeScript e uma forma muito mais elegante de compartilhar lógica entre componentes.
Se você ainda não mergulhou de cabeça na Composition API, esse é o momento. A curva de aprendizado existe, especialmente se você vem de anos de Options API, mas uma vez que você entende ref, computed, onMounted e a ideia de composables, o código começa a fluir de forma muito mais natural e organizada.
E a melhor parte: você pode começar devagar, introduzindo a Composition API gradualmente nos seus projetos, sem precisar jogar tudo fora.