Vue 3: Composition API in Practice

Use Vue 3: Composition API in Practice para organizar lógica por funcionalidade, criar composables reutilizáveis e reduzir a complexidade de componentes.

Avatar de Gabriel
Gabriel
Vue 3: Composition API in Practice

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/watchEffect e o setup() (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, computed e watch.
  • 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)

Visualizar a diferença entre organizar por opções (data/methods) vs por funcionalidades (busca/filtros/paginação). - Prompt: `Diagrama comparando dois componentes Vue: à esquerda blocos rotulados data
Visualizar a diferença entre organizar por opções (data/methods) vs por funcionalidades (busca/filtros/paginação). - Prompt: `Diagrama comparando dois componentes Vue: à esquerda blocos rotulados data

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:

  1. 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
   }
 }

}) ```

  1. <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> ```

  1. 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 .value no código TypeScript.
  • Colocar .value dentro 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 é:

  1. Entrada: props, emit/context.
  2. Estado: ref, reactive.
  3. Lógica derivada: computed.
  4. Efeitos: watch, watchEffect, hooks de ciclo de vida.
  5. 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:

  1. Imports.
  2. defineProps / defineEmits.
  3. Estado reativo (ref / reactive).
  4. computed.
  5. watch/watchEffect/hooks.
  6. Funções de ação (ex.: fetchItems, goToPage).
  7. 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, sem ref/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)

Representar a ideia de um composable como um “tijolo” reutilizável. - Prompt: `Ilustração abstrata de blocos modulares representando funções reutilizáveis, ícones de código, setas mostrando reuso em m
Representar a ideia de um composable como um “tijolo” reutilizável. - Prompt: `Ilustração abstrata de blocos modulares representando funções reutilizáveis, ícones de código, setas mostrando reuso em m

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 fetchFn e buildParams: 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

Mostrar visualmente a tela que serve como fio condutor do artigo. - Prompt: `Interface de dashboard com tabela de dados, barra de busca, filtros por status e controles de paginação, estilo UI moderna,
Mostrar visualmente a tela que serve como fio condutor do artigo. - Prompt: `Interface de dashboard com tabela de dados, barra de busca, filtros por status e controles de paginação, estilo UI moderna,

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, watch e created.

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

Comunicar a transformação de um componente bagunçado em algo organizado por composables. - Prompt: `Imagem dividida em duas partes: à esquerda código emaranhado e confuso, à direita blocos de código o
Comunicar a transformação de um componente bagunçado em algo organizado por composables. - Prompt: `Imagem dividida em duas partes: à esquerda código emaranhado e confuso, à direita blocos de código o

Agora vamos separar dois composables simples:

  • usePagination
  • useAsyncData

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.
  • usePagination e useAsyncData podem 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:

  1. Escolha um componente “dolorido” no seu projeto atual
  2. De preferência, algo como uma tela de dashboard com busca e filtros.
  3. Reescreva-o internamente com <script setup lang="ts">, sem mexer na interface.

  4. Identifique responsabilidades claras

  5. Busca, filtros, paginação, loading/erro, etc.
  6. Agrupe a lógica relacionada junto (estado + computed + watchers + chamadas de API).

  7. Extraia um primeiro composable

  8. Comece pequeno: usePagination ou useAsyncData.
  9. Use em pelo menos dois componentes para validar o design e os tipos.

  10. Refine o estilo do seu time

  11. Combine convenções: nome de composables (useAlgo), estrutura interna do <script setup>, quando usar ref vs reactive, como tipar fetchFn, etc.

  12. Conecte com a comunidade

  13. Traga seus casos reais, dúvidas e padrões que você descobriu:

Gostou do artigo?

Compartilhe com seus amigos e ajude a espalhar conhecimento!