Este artigo é para quem já usa Vue, gosta da Options API, mas começou a sentir que os componentes estão ficando grandes demais e difíceis de manter.
TL;DR
- Vue 3: Composition API in Practice não é “outro Vue”: é um jeito diferente de organizar a mesma lógica.
- Em vez de separar por “caixinhas” (
data,methods,computed), você passa a organizar o código por funcionalidades (busca, filtros, paginação, loading…). - Os blocos fundamentais são:
ref,reactive,computed,watch/watchEffecte osetup()(ou<script setup>). - Composables são funções que encapsulam uma responsabilidade reativa (ex.:
useSearchableList,usePagination,useAsyncData). - A Composition API brilha em componentes médios/grandes e em lógica reutilizável; a Options API continua ótima para componentes simples.
- Comece aos poucos: um componente chato hoje é uma boa cobaia para refatorar usando Composition API.
Se quiser ir além depois da leitura, há um artigo complementar em inglês chamado “Vue 3 Composition API in Practice” em um contexto mais amplo de engenharia de software, disponível no site da DW Corp: artigo “Vue 3 Composition API in Practice” da DW Corp.
1. Por que sair do “Vue básico” e olhar para a Composition API?
Se você já montou um dashboard com busca + filtros + paginação + loading + estado de erro em Vue, provavelmente sentiu algo assim:
- O componente começou pequeno… e virou um monstro de centenas de linhas.
- A lógica está espalhada entre
data,methods,computedewatch. - Reaproveitar a mesma lógica de busca em outra tela significa… copiar e colar (ou usar mixins meio mágicos).
Quando a Options API começa a doer na prática
A Options API funciona muito bem quando:
- O componente tem uma única responsabilidade clara.
- A lógica é pequena o suficiente para caber em poucas opções.
Agora pense de novo no dashboard com:
- Busca
- Filtros por status
- Paginação
- Loading e tratamento de erro
- Talvez até atualização automática (polling)
Você acaba com algo assim:
data:items,searchTerm,filters,currentPage,isLoading,error, …computed:filteredItems,paginatedItems,hasMorePages, …methods:fetchItems,applyFilters,handleSearch,goToPage,reload, …watch:searchTerm,filters,currentPage…
Ou seja: tudo misturado, e é difícil bater o olho e entender o que faz parte de qual funcionalidade.
O que você vai aprender aqui
Usando o exemplo de dashboard com busca + filtros + paginação, vamos ver na prática como:
- Organizar melhor a lógica dentro de um componente usando Composition API.
- Extrair partes dessa lógica em composables reutilizáveis (por exemplo,
useSearchableList,usePagination,useAsyncData). - Entender quando faz sentido usar a Composition API e quando a Options API continua uma boa escolha.
2. O que é a Composition API (sem buzzword)

Não é “outro Vue”
A Composition API não é um framework novo, nem um “modo avançado secreto”. Ela é uma forma alternativa de declarar a mesma lógica que você já escreveria com a Options API.
Mudança de mentalidade:
- Options API: você separa o código por tipo de coisa
(data,methods,computed,watch…). - Composition API: você separa o código por funcionalidade
(busca, filtros, paginação, estado da requisição…).
Na prática, isso significa:
- Tudo que é relacionado a “busca” pode ficar junto (estado + computados + watchers + chamadas de API).
- Tudo que é “paginação” fica junto.
- Tudo que é “estado de requisição” (loading/erro/dados) fica junto.
Onde a Composition API aparece
Você encontra a Composition API em três lugares principais:
setup()dentro de componentes
```ts import { defineComponent, ref } from 'vue'
export default defineComponent({ setup(props, context) { const searchTerm = ref('')
// usar ref, reactive, computed, watch...
return {
searchTerm
// o que o template enxerga
}
}
}) ```
<script setup>em Single File Components (SFC)
```vue
<script setup lang="ts">
import { ref } from 'vue'
const searchTerm = ref<string>('')
</script>
<template> <input v-model="searchTerm" /> </template> ```
- Funções reutilizáveis (composables)
```ts import { ref, computed } from 'vue'
export interface SearchableItem { name: string [key: string]: unknown }
export function useSearchableList<T extends SearchableItem>( initialItems: T[] = [] ) { const items = ref<T[]>(initialItems) const searchTerm = ref<string>('')
const filteredItems = computed<T[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
return {
items,
searchTerm,
filteredItems
}
} ```
Essa função pode ser importada em qualquer componente, reutilizando a mesma lógica de forma clara.
Vue 3: Composition API in Practice vs Options API
- Options API
- Muito boa para começar.
- Ótima para componentes simples.
-
O “shape” do componente é previsível (sempre
data,methods, etc.). -
Vue 3: Composition API in Practice
- Fica mais confortável em componentes médios/grandes.
- É mais poderosa para reutilizar lógica.
- Facilita separar o código por domínio/funcionalidade, não por “tipo de opção”.
Ambas coexistem no Vue 3. A própria documentação oficial da Composition API tem uma seção de FAQ que discute motivações, benefícios e trade-offs com detalhes.
3. Fundamentos da Composition API em prática
Antes de falar de composables, vamos passar pelos blocos básicos, já com TypeScript.
3.1. Reatividade com ref e reactive
ref: valores simples e pontuais
ref cria um valor reativo “embalado” em um objeto com a propriedade .value:
import { ref } from 'vue'
const searchTerm = ref<string>('') // string reativa
const currentPage = ref<number>(1) // número reativo
const isLoading = ref<boolean>(false) // booleano reativo
- Dentro do código, você lê/escreve usando
.value:
ts
searchTerm.value = 'vue'
console.log(currentPage.value)
- No template, você não usa
.value:
vue
<template>
<input v-model="searchTerm" />
<p>Página atual: {{ currentPage }}</p>
</template>
Pitfall típico:
- Esquecer o
.valueno código TypeScript. - Colocar
.valuedentro do template.
reactive: objetos e “estado agrupado”
Quando você tem um estado que naturalmente é um objeto (ex.: filtros combinados, dados de formulário), reactive costuma ficar mais natural:
import { reactive } from 'vue'
interface Filters {
status: 'all' | 'open' | 'closed'
minDate: Date | null
maxDate: Date | null
}
const filters = reactive<Filters>({
status: 'all',
minDate: null,
maxDate: null
})
Você acessa as propriedades normalmente:
filters.status = 'open'
console.log(filters.minDate)
Mas há um detalhe importante:
Se você desestruturar um objeto
reactive, a reatividade se perde nas variáveis desestruturadas.
Exemplo problemático:
// ❌ NÃO faça isso
const { status, minDate } = filters
// status e minDate agora são cópias, não são reativas
Regras mentais rápidas:
ref- Para valores primitivos (string, number, boolean).
-
Para estados que você manipula isoladamente (ex.:
currentPage,searchTerm). -
reactive - Para objetos que representam um estado coeso (ex.:
filters,form,queryParams). - Evite desestruturar esse objeto, ou saiba exatamente o que está fazendo.
3.2. Derivando valores com computed
computed gera um valor que depende de outros reativos e é recalculado automaticamente quando as dependências mudam.
No nosso dashboard:
import { ref, computed } from 'vue'
interface Item {
id: number
name: string
status: 'published' | 'draft'
}
const items = ref<Item[]>([
{ id: 1, name: 'Vue 3 Guide', status: 'published' },
{ id: 2, name: 'Composition API Tips', status: 'draft' }
])
const searchTerm = ref<string>('vue')
const filteredItems = computed<Item[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
No template:
<template>
<input v-model="searchTerm" placeholder="Buscar..." />
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
Aqui você separa claramente:
- Estados de origem (
items,searchTerm). - Lógica derivada (
filteredItems).
3.3. Reagir a mudanças com watch e watchEffect
Às vezes você precisa de efeitos colaterais:
- Chamar uma API quando um filtro muda.
- Salvar algo no
localStorage. - Sincronizar a URL com o estado interno.
Quando watch faz sentido
watch observa uma ou mais fontes reativas específicas e executa uma função quando elas mudam:
import { ref, watch } from 'vue'
const searchTerm = ref<string>('')
const currentPage = ref<number>(1)
watch(
[searchTerm, currentPage],
([newSearch, newPage], [oldSearch, oldPage]) => {
// você tem acesso ao valor novo e ao antigo
fetchItems({ search: newSearch, page: newPage })
}
)
async function fetchItems(params: { search: string; page: number }) {
// chamada à API...
}
Cenários típicos para watch:
- Chamadas a API em resposta a mudanças de filtros/paginação.
- Lógica de debounce/throttle manual.
- Persistência de estado (ex.: salvar configurações do usuário).
watchEffect vs watch
watchEffect roda a função imediatamente e rastreia automaticamente as dependências reativas usadas dentro dela:
import { ref, watchEffect } from 'vue'
const searchTerm = ref<string>('')
const currentPage = ref<number>(1)
watchEffect(() => {
fetchItems({ search: searchTerm.value, page: currentPage.value })
})
Diferenças importantes:
watch:- Você declara explicitamente o que está sendo observado.
-
Ideal quando você quer controle fino (comparar valores anteriores, configurar
flush, etc.). -
watchEffect: - Mais rápido de escrever.
- Útil para efeitos simples atrelados a vários estados.
- Pode ficar confuso se a função começa a acessar muitos estados (as dependências ficam “escondidas”).
4. De setup() a <script setup>: organizando o componente
4.1. Anatomia de um componente com Composition API
Vamos imaginar um componente DashboardList.vue com busca, filtros, paginação, loading e erro.
Com setup(), a estrutura mental é:
- Entrada:
props,emit/context. - Estado:
ref,reactive. - Lógica derivada:
computed. - Efeitos:
watch,watchEffect, hooks de ciclo de vida. - Retorno: o que o template pode usar.
Esquematicamente:
import {
defineComponent,
ref,
reactive,
computed,
watch,
onMounted
} from 'vue'
interface Filters {
status: string
}
interface Item {
id: number
name: string
}
export default defineComponent({
props: {
initialStatus: { type: String, default: 'all' }
},
setup(props, { emit }) {
// 1. estado
const searchTerm = ref<string>('')
const currentPage = ref<number>(1)
const filters = reactive<Filters>({
status: props.initialStatus
})
const items = ref<Item[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
// 2. derivados
const filteredItems = computed<Item[]>(() => {
// usa items, searchTerm, filters...
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
// 3. efeitos
async function fetchItems() {
// chamada à API usando filtros, paginação...
}
watch(
[searchTerm, () => filters.status, currentPage],
() => {
fetchItems()
}
)
// 4. ciclo de vida
onMounted(() => {
fetchItems()
})
// 5. retorno para o template
return {
searchTerm,
filters,
currentPage,
items,
filteredItems,
isLoading,
error,
fetchItems
}
}
})
Pitfalls comuns:
- Declarar uma ref/computed e esquecer de retorná-la de
setup(). - Entupir um único bloco
setup()sem separar visualmente por responsabilidade.
4.2. Usando <script setup> no dia a dia
<script setup> é um “atalho” para Composition API em SFCs:
- Não precisa declarar
setup()manualmente. - Tudo que você declara no
<script setup>fica automaticamente disponível no template. - Menos “cerimônia”, mais foco na lógica.
Exemplo equivalente, com <script setup> e TypeScript:
<script setup lang="ts">
import {
ref,
reactive,
computed,
watch,
onMounted
} from 'vue'
interface Filters {
status: string
}
interface Item {
id: number
name: string
}
// props
const props = defineProps<{
initialStatus?: string
}>()
// 1. estado
const searchTerm = ref<string>('')
const currentPage = ref<number>(1)
const filters = reactive<Filters>({
status: props.initialStatus ?? 'all'
})
const items = ref<Item[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
// 2. derivados
const filteredItems = computed<Item[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
// 3. efeitos
async function fetchItems(): Promise<void> {
// chamada à API
}
watch(
[searchTerm, () => filters.status, currentPage],
() => {
fetchItems()
}
)
// 4. ciclo de vida
onMounted(() => {
fetchItems()
})
</script>
<template>
<!-- usa searchTerm, filters, filteredItems, etc. diretamente -->
</template>
Uma ordem simples que funciona bem na maioria dos componentes:
- Imports.
defineProps/defineEmits.- Estado reativo (
ref/reactive). computed.watch/watchEffect/hooks.- Funções de ação (ex.:
fetchItems,goToPage). - Chamadas iniciais (
onMounted, etc.).
Sempre que fizer sentido, agrupe visualmente por “feature”:
- Bloco de busca
- Bloco de filtros
- Bloco de paginação
- Bloco de estado da requisição
Isso prepara o terreno para o próximo passo: extrair composables.
5. Composables: extraindo e reutilizando lógica de verdade
5.1. O que é um composable (e o que não é)
Um composable é:
Uma função que usa a Composition API (
ref,reactive,computed,watch, hooks de ciclo de vida) para encapsular uma responsabilidade reativa.
Exemplos de responsabilidades típicas:
- Gerenciar busca e filtros de uma lista (
useSearchableList). - Controlar paginação (
usePagination). - Centralizar lógica de chamadas assíncronas com loading/erro (
useAsyncData).
O que não é necessariamente um composable:
- Funções puras de utilidade, que só recebem dados e retornam resultado (ex.:
formatCurrency,parseDate).
Essas podem (e devem) continuar como helpers normais, semref/reactive.
Diferença em relação a mixins:
- Mixins injetam propriedades/métodos de forma “mágica” no componente.
- Composables são funções explícitas: você importa, chama e recebe um “pacote” de estado e funções.
5.2. Construindo um composable passo a passo (com TypeScript)

Vamos pegar uma parte da tela de dashboard: listagem com busca e loading.
1) Lógica inline no componente
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
// import { api } from '@/services/api'
interface Item {
id: number
name: string
}
const searchTerm = ref<string>('')
const items = ref<Item[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
async function fetchItems(): Promise<void> {
isLoading.value = true
error.value = null
try {
// chamada à API
// const response = await api.get<Item[]>('/items', {
// params: { search: searchTerm.value }
// })
// items.value = response.data
// para exemplo, vamos simular:
items.value = [
{ id: 1, name: 'Vue 3 Guide' },
{ id: 2, name: 'Composition API in Practice' }
]
} catch (err) {
error.value = 'Erro ao carregar itens'
} finally {
isLoading.value = false
}
}
const filteredItems = computed<Item[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(term)
)
})
watch(searchTerm, () => {
fetchItems()
})
onMounted(() => {
fetchItems()
})
</script>
Funciona, mas essa combinação de estado + fetch + loading + erro provavelmente vai se repetir.
2) Extraindo para useSearchableList()
// useSearchableList.ts
import { ref, computed, watch, onMounted } from 'vue'
export interface UseSearchableListParams<TSearchParams> {
fetchFn: (params: TSearchParams) => Promise<unknown[]>
buildParams: (search: string) => TSearchParams
immediate?: boolean
}
export function useSearchableList<TItem, TSearchParams = { search: string }>({
fetchFn,
buildParams,
immediate = true
}: UseSearchableListParams<TSearchParams>) {
const searchTerm = ref<string>('')
const items = ref<TItem[]>([])
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
async function load(): Promise<void> {
isLoading.value = true
error.value = null
try {
const params = buildParams(searchTerm.value)
const data = await fetchFn(params)
items.value = data as TItem[]
} catch (err) {
error.value =
err instanceof Error ? err.message : 'Erro ao carregar'
} finally {
isLoading.value = false
}
}
const filteredItems = computed<TItem[]>(() => {
const term = searchTerm.value.toLowerCase()
return items.value.filter((item: any) =>
String(item.name ?? '')
.toLowerCase()
.includes(term)
)
})
watch(searchTerm, () => {
void load()
})
if (immediate) {
onMounted(() => {
void load()
})
}
return {
// estado
searchTerm,
items,
filteredItems,
isLoading,
error,
// ações
reload: load
}
}
Pontos importantes:
- O composable recebe
fetchFnebuildParams: ele não sabe como buscar; só coordena. - Ele expõe apenas o que faz sentido para o componente: estado e ações.
3) Usando o composable em um componente
<script setup lang="ts">
import { useSearchableList } from '@/composables/useSearchableList'
// import { api } from '@/services/api'
interface Item {
id: number
name: string
}
const {
searchTerm,
filteredItems,
isLoading,
error,
reload
} = useSearchableList<Item, { search: string }>({
buildParams: (search: string) => ({ search }),
fetchFn: async ({ search }) => {
// const response = await api.get<Item[]>('/items', { params: { search } })
// return response.data
// exemplo simplificado:
return [
{ id: 1, name: `Item com filtro: ${search}` }
]
},
immediate: true
})
</script>
<template>
<input v-model="searchTerm" placeholder="Buscar..." />
<button @click="reload">Recarregar</button>
<p v-if="isLoading">Carregando...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
Você pode reaproveitar o mesmo composable em outra tela, mudando apenas o tipo Item e a função fetchFn.
5.3. Boas práticas e armadilhas em composables
Boas práticas:
- Dê a cada composable uma responsabilidade clara:
useSearchableList→ busca em listas.usePagination→ paginação.-
useAsyncData→ estado de requisição assíncrona. -
Não tente “espelhar” o componente inteiro dentro de um composable:
-
Composables são tijolinhos que o componente combina.
-
Evite composables que sabem demais sobre UI:
- Deixe texto, rótulo, cores e layout para os componentes.
- Composables cuidam de estado e regras.
Trade-off com TypeScript:
- Composables ficam um pouco mais verbosos (interfaces, generics).
- Em compensação, o TypeScript ajuda a:
- Documentar parâmetros e retornos.
- Evitar erros de uso (tipar
fetchFn, estados, etc.).
6. Caso real: organizando um componente “bagunçado” com Composition API

6.1. O componente “antes”: Options API com responsabilidades misturadas
Um DashboardList.vue com Options API poderia ser algo assim:
import { defineComponent } from 'vue'
// import { api } from '@/services/api'
interface Item {
id: number
name: string
}
export default defineComponent({
data() {
return {
items: [] as Item[],
searchTerm: '',
statusFilter: 'all',
currentPage: 1,
pageSize: 20,
totalItems: 0,
isLoading: false,
error: null as string | null
}
},
computed: {
filteredItems(): Item[] {
// filtro por searchTerm e status
return this.items
},
paginatedItems(): Item[] {
// fatia filteredItems de acordo com currentPage/pageSize
return this.filteredItems
}
},
methods: {
async fetchItems(): Promise<void> {
this.isLoading = true
this.error = null
try {
// const response = await api.get('/items', {
// params: {
// search: this.searchTerm,
// status: this.statusFilter,
// page: this.currentPage,
// pageSize: this.pageSize
// }
// })
// this.items = response.data.items
// this.totalItems = response.data.total
} catch (err) {
this.error = 'Erro ao carregar itens'
} finally {
this.isLoading = false
}
},
handleSearchChange(): void {
this.currentPage = 1
void this.fetchItems()
},
handleStatusChange(): void {
this.currentPage = 1
void this.fetchItems()
},
goToPage(page: number): void {
this.currentPage = page
void this.fetchItems()
}
},
watch: {
searchTerm() {
this.handleSearchChange()
},
statusFilter() {
this.handleStatusChange()
}
},
created() {
void this.fetchItems()
}
})
Funciona, mas:
- Lógica de busca, filtro, paginação, loading/erro e requisições está fortemente acoplada.
- Reutilizar a paginação em outro componente exige copiar boa parte do código.
- Entender o fluxo completo exige “passear” por
data,computed,methods,watchecreated.
6.2. Migrando mentalmente para Composition API
Primeiro passo: ainda sem composables, apenas reorganizar por funcionalidade em <script setup>.
<script setup lang="ts">
import {
ref,
computed,
watch,
onMounted
} from 'vue'
// import { api } from '@/services/api'
interface Item {
id: number
name: string
}
// --- Estado base ---
const items = ref<Item[]>([])
const totalItems = ref<number>(0)
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
// --- Busca e filtro ---
const searchTerm = ref<string>('')
const statusFilter = ref<string>('all')
// --- Paginação ---
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
// --- Derivados ---
const filteredItems = computed<Item[]>(() => {
// filtro por searchTerm/status local, se necessário
return items.value
})
const paginatedItems = computed<Item[]>(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredItems.value.slice(start, end)
})
// --- Requisição ---
async function fetchItems(): Promise<void> {
isLoading.value = true
error.value = null
try {
// const response = await api.get('/items', {
// params: {
// search: searchTerm.value,
// status: statusFilter.value,
// page: currentPage.value,
// pageSize: pageSize.value
// }
// })
// items.value = response.data.items
// totalItems.value = response.data.total
} catch (err) {
error.value = 'Erro ao carregar itens'
} finally {
isLoading.value = false
}
}
// --- Reações ---
watch([searchTerm, statusFilter], () => {
currentPage.value = 1
void fetchItems()
})
watch(currentPage, () => {
void fetchItems()
})
onMounted(() => {
void fetchItems()
})
</script>
Mesmo sem extrair nada, já dá para enxergar melhor cada responsabilidade.
6.3. Extraindo composables realmente úteis

Agora vamos separar dois composables simples:
usePaginationuseAsyncData
usePagination
// usePagination.ts
import { ref, computed } from 'vue'
export interface UsePaginationOptions {
pageSize?: number
}
export function usePagination(options: UsePaginationOptions = {}) {
const perPage = ref<number>(options.pageSize ?? 20)
const currentPage = ref<number>(1)
const totalItems = ref<number>(0)
const totalPages = computed<number>(() => {
if (perPage.value === 0) return 0
return Math.ceil(totalItems.value / perPage.value)
})
function goToPage(page: number): void {
currentPage.value = page
}
function resetPage(): void {
currentPage.value = 1
}
return {
currentPage,
perPage,
totalItems,
totalPages,
goToPage,
resetPage
}
}
useAsyncData
// useAsyncData.ts
import { ref } from 'vue'
export function useAsyncData<TData, TParams = void>(
fetchFn: (params: TParams) => Promise<TData>
) {
const data = ref<TData | null>(null)
const isLoading = ref<boolean>(false)
const error = ref<string | null>(null)
async function load(params: TParams): Promise<void> {
isLoading.value = true
error.value = null
try {
data.value = await fetchFn(params)
} catch (err) {
error.value =
err instanceof Error ? err.message : 'Erro ao carregar'
} finally {
isLoading.value = false
}
}
return {
data,
isLoading,
error,
load
}
}
Reescrevendo o componente com composables
<script setup lang="ts">
import {
ref,
computed,
watch,
onMounted
} from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useAsyncData } from '@/composables/useAsyncData'
// import { api } from '@/services/api'
interface Item {
id: number
name: string
}
// --- Busca e filtro ---
const searchTerm = ref<string>('')
const statusFilter = ref<string>('all')
// --- Paginação ---
const {
currentPage,
perPage,
totalItems,
totalPages,
goToPage,
resetPage
} = usePagination({ pageSize: 20 })
// --- Requisição assíncrona ---
const {
data: items,
isLoading,
error,
load: loadItems
} = useAsyncData<Item[], {
search: string
status: string
page: number
pageSize: number
}>(async ({ search, status, page, pageSize }) => {
// const response = await api.get('/items', {
// params: { search, status, page, pageSize }
// })
// totalItems.value = response.data.total
// return response.data.items as Item[]
// exemplo simplificado:
totalItems.value = 100
return [
{ id: 1, name: `Item ${search} - página ${page}` }
]
})
// --- Derivados ---
const filteredItems = computed<Item[]>(() => {
const term = searchTerm.value.toLowerCase()
return (items.value ?? []).filter(item =>
item.name.toLowerCase().includes(term)
)
})
// --- Orquestração ---
function reload(): void {
void loadItems({
search: searchTerm.value,
status: statusFilter.value,
page: currentPage.value,
pageSize: perPage.value
})
}
// --- Reações ---
watch([searchTerm, statusFilter], () => {
resetPage()
reload()
})
watch(currentPage, () => {
reload()
})
onMounted(() => {
reload()
})
</script>
<template>
<!-- template foca em montar a UI, não em gerenciar detalhes de estado -->
<input v-model="searchTerm" placeholder="Buscar..." />
<select v-model="statusFilter">
<option value="all">Todos</option>
<option value="open">Abertos</option>
<option value="closed">Fechados</option>
</select>
<button @click="reload">Recarregar</button>
<p v-if="isLoading">Carregando...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
<nav>
<button
:disabled="currentPage === 1"
@click="goToPage(currentPage - 1)"
>
Anterior
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button
:disabled="currentPage === totalPages"
@click="goToPage(currentPage + 1)"
>
Próxima
</button>
</nav>
</template>
Resultado:
- O componente está mais focado em orquestrar busca/filtro/paginação do que em detalhes de implementação.
usePaginationeuseAsyncDatapodem ser reutilizados em outras telas.- Se a regra de paginação mudar, você provavelmente mexe só em
usePagination.
Trade-offs:
- Mais arquivos (composables) para manter.
- Exige alinhamento no time sobre como desenhar composables.
- Em projetos médios/grandes, o ganho de clareza e reuso costuma compensar.
7. Quando usar (ou não) a Vue 3: Composition API in Practice
Casos em que a Composition API brilha
Ela costuma fazer muita diferença quando:
- Você tem componentes médios/grandes com várias responsabilidades (como dashboards com busca, filtros, paginação, loading, erro…).
- A lógica de negócios é complexa e compartilhada (regras de domínio, formulários complexos, integrações com APIs externas).
- O projeto é pensado para manutenção em equipe por bastante tempo.
Ter composables bem definidos (usePagination, useAsyncData, useSearchableList) cria um vocabulário comum entre o time.
Casos em que a Options API ainda faz sentido
A Options API continua totalmente válida quando:
- O componente é simples e isolado:
- Um botão especializado.
- Um card com 1–2 estados.
- O time ainda está aprendendo Vue e não sente dor real com Options API.
- Você quer prototipar algo rápido sem se preocupar tanto com estrutura futura.
Uma abordagem equilibrada:
- Novo código mais complexo → comece com
<script setup>e Composition API. - Componentes pequenos → Options API ainda é ok.
- Refatore aos poucos apenas o que dói hoje.
Estratégias de adoção gradual
- Use
<script setup>em componentes novos, mesmo sem composables de início. - Crie seus primeiros composables para problemas bem concretos:
- Padrão de loading/erro em chamadas de API.
- Paginação já existente em duas telas.
- Quando algo for usado em mais de um componente, considere extrair para um composable.
Proximos Passos
Se você chegou até aqui, já tem base suficiente para usar Vue 3: Composition API in Practice no seu dia a dia. Sugestão de próximos passos:
- Escolha um componente “dolorido” no seu projeto atual
- De preferência, algo como uma tela de dashboard com busca e filtros.
-
Reescreva-o internamente com
<script setup lang="ts">, sem mexer na interface. -
Identifique responsabilidades claras
- Busca, filtros, paginação, loading/erro, etc.
-
Agrupe a lógica relacionada junto (estado + computed + watchers + chamadas de API).
-
Extraia um primeiro composable
- Comece pequeno:
usePaginationouuseAsyncData. -
Use em pelo menos dois componentes para validar o design e os tipos.
-
Refine o estilo do seu time
-
Combine convenções: nome de composables (
useAlgo), estrutura interna do<script setup>, quando usarrefvsreactive, como tiparfetchFn, etc. -
Conecte com a comunidade
- Traga seus casos reais, dúvidas e padrões que você descobriu:
- Participe da comunidade SCCB — perfil da Software Craftsmanship Brasília no Instagram
- Veja os próximos eventos — agenda de eventos da SCCB no Instagram

