404 Not Found

404 Not Found


nginx

Composables: Funções Composable

A equipe do Charlie tem muita lógica duplicada—a formatação de preços é escrita separadamente em 10 componentes, e a lógica de busca está espalhada em 3 páginas. O recurso de favoritos da Alice precisa compartilhar estado entre páginas. Bob precisa de uma lógica genérica de paginação. Composables são a solução—extraia a lógica em funções reutilizáveis.

1. O Que Você Vai Aprender


2. Uma História Real de um Arquiteto

(1) Dor: Lógica duplicada espalhada pelo código

A MegaShop tem 10 componentes que precisam formatar preços como "USD 2.999,99", e cada um requer uma implementação customizada de Intl.NumberFormat. Alice implementou o recurso de "Favoritos" tanto na homepage quanto na página de detalhes do produto, mas a lógica é inconsistente—itens favoritados na homepage não aparecem na página de detalhes do produto.

(2) A Solução Composable

Uma vez extraído em um Composable, defina uma vez e reutilize em todo lugar:

TYPESCRIPT
// composables/usePriceFormat.ts
export function usePriceFormat(price: Ref<number>, currency = 'USD') {
  return computed(() => new Intl.NumberFormat('en-US', {
    style: 'currency', currency
  }).format(price.value))
}

(3) Benefícios: DRY + Consistência

Dez componentes compartilham a mesma lógica de formatação de preços, e o status da lista de desejos permanece consistente em todas as páginas graças ao useWishlist. O volume de código foi reduzido em 60%, e os bugs em 80%.


3. Visão Geral dos Composables Built-in

(1) Grafo de Dependência dos Composables Built-in

100%
graph TB
    A[Composables Built-in do Nuxt] --> B[Busca de Dados]
    A --> C[Gerenciamento de Estado]
    A --> D[Navegação]
    A --> E[Gestão de Head]
    A --> F[Contexto]
    A --> G[Utilitários]

    B --> B1[useFetch]
    B --> B2[useAsyncData]
    B --> B3[$fetch]
    C --> C1[useState]
    C --> C2[useCookie]
    D --> D1[useRouter]
    D --> D2[useRoute]
    D --> D3[navigateTo]
    E --> E1[useHead]
    E --> E2[useSeoMeta]
    F --> F1[useRequestHeaders]
    F --> F2[useRuntimeConfig]
    G --> G1[useAppConfig]
    G --> G2[useHydration]

(2) Referência Rápida dos Composables Built-in Core

Composable Propósito Suporta SSR Valor de Retorno
useFetch Recuperação de Dados { data, pending, error, refresh }
useAsyncData Assíncrono Geral { data, pending, error, refresh }
useState Estado compartilhado Ref
useCookie Operações de Cookie Ref
useRouter Instância do Router ⚠️ Apenas cliente Router
useRoute Rota Atual Route
useHead Gestão de Head void
useSeoMeta Metadados SEO void
useRuntimeConfig Configuração de Runtime RuntimeConfig
useRequestHeaders Headers da Requisição ✅ Apenas servidor Headers

(1) ▶Exemplo: Compartilhando Estado Entre Componentes com useState

VUE
<script setup lang="ts">
// Compartilhar estado de notificações entre componentes
const notifications = useState<Notification[]>('notifications', () => [])

function addNotification(message: string, type: 'success' | 'error' = 'success') {
  notifications.value.push({ id: Date.now(), message, type })
  setTimeout(() => {
    notifications.value = notifications.value.filter(n => n.id !== Date.now())
  }, 3000)
}
</script>

Saída:

TEXT
// Execução Bem-sucedida

4. Composable Personalizado

(1) Convenções de Nomenclatura e Diretório

Regra Descrição Exemplo
Diretório composables/ composables/useCart.ts
Nome prefixo use usePriceFormat
Export Export Nomeada export function usePriceFormat()
Tipo Interface definida no mesmo arquivo interface PriceOptions {}
Auto-import ✅ Automático Sem necessidade de import manual

(1) ▶Exemplo: Formatação de preços com usePriceFormat

TYPESCRIPT
// composables/usePriceFormat.ts
interface PriceFormatOptions {
  currency?: string
  locale?: string
  showDecimals?: boolean
}

export function usePriceFormat(
  price: Ref<number> | number,
  options: PriceFormatOptions = {}
) {
  const { currency = 'USD', locale = 'en-US', showDecimals = true } = options

  const formatted = computed(() => {
    const value = unref(price)
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency,
      minimumFractionDigits: showDecimals ? 2 : 0,
      maximumFractionDigits: showDecimals ? 2 : 0
    }).format(value)
  })

  return { formatted }
}

Saída:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: Favoritos com useWishlist

TYPESCRIPT
// composables/useWishlist.ts
export function useWishlist() {
  const wishlist = useState<number[]>('wishlist', () => [])

  function toggle(productId: number) {
    const index = wishlist.value.indexOf(productId)
    if (index === -1) {
      wishlist.value.push(productId)
    } else {
      wishlist.value.splice(index, 1)
    }
  }

  function isWishlisted(productId: number): boolean {
    return wishlist.value.includes(productId)
  }

  const count = computed(() => wishlist.value.length)

  return { wishlist, toggle, isWishlisted, count }
}

Saída:

TEXT
// Execução Bem-sucedida

(3) ▶Exemplo: Lógica de busca com useProductSearch

TYPESCRIPT
// composables/useProductSearch.ts
interface SearchParams {
  query?: string
  category?: string
  minPrice?: number
  maxPrice?: number
  sortBy?: 'price' | 'name' | 'rating'
  page?: number
}

export function useProductSearch(initialParams: SearchParams = {}) {
  const params = reactive<SearchParams>({
    query: '',
    category: '',
    minPrice: 0,
    maxPrice: 10000,
    sortBy: 'rating',
    page: 1,
    ...initialParams
  })

  const { data, pending, error, refresh } = useFetch('/api/products/search', {
    query: params,
    watch: [params],
    default: () => ({ items: [], total: 0 })
  })

  function resetFilters() {
    params.query = ''
    params.category = ''
    params.minPrice = 0
    params.maxPrice = 10000
    params.sortBy = 'rating'
    params.page = 1
  }

  return { params, data, pending, error, refresh, resetFilters }
}

Saída:

TEXT
// Execução Bem-sucedida

5. O Padrão de Design de Composable

(1) Comparação de Padrões de Design

Padrão Características Casos de Uso Exemplos
Composable Síncrono Retorna computed/ref Formatação/Computação usePriceFormat
Composable Assíncrono Chamada interna a useFetch Recuperação de dados useProductSearch
Reatividade de Parâmetros Refresh Automático via watch Filtro/Busca useProductSearch
Compartilhamento de Estado useState interno Estado Entre Componentes useWishlist
Composição Chamando Outros Composables Lógica Complexa useCheckout

(1) ▶Exemplo: Composição—useCheckout

TYPESCRIPT
// composables/useCheckout.ts
export function useCheckout() {
  // Compor outros composables
  const cartStore = useCartStore()
  const { formatted: total } = usePriceFormat(
    computed(() => cartStore.totalPrice)
  )
  const { isAuthenticated, user } = storeToRefs(useUserStore())

  const isProcessing = ref(false)
  const orderId = ref<string | null>(null)

  async function processCheckout() {
    if (!isAuthenticated.value) {
      navigateTo('/login')
      return
    }

    isProcessing.value = true
    try {
      const order = await $fetch('/api/orders', {
        method: 'POST',
        body: {
          items: cartStore.items,
          userId: user.value?.id,
          total: cartStore.totalPrice
        }
      })
      orderId.value = order.id
      cartStore.clearCart()
      navigateTo(`/orders/${order.id}`)
    } catch (err) {
      console.error('Falha no checkout:', err)
    } finally {
      isProcessing.value = false
    }
  }

  return { total, isProcessing, orderId, processCheckout, isAuthenticated }
}

Saída:

TEXT
// Execução Bem-sucedida

(2) Princípio de Design de Composable

Princípio Explicação Contraexemplo
Responsabilidade Única Um Composable faz uma coisa useShopAndCart()
Parâmetro é responsivo Aceita parâmetros Ref Aceita apenas valores brutos
Desestruturação de Valor de Retorno Retorna Objeto Nomeado Retorna Array
Nomenclatura Clara use + verbo/substantivo doStuff()
Sem efeitos colaterais Não modifica o DOM diretamente Opera o DOM internamente

6. Exemplo Completo: A Arquitetura de Composables da MegaShop

TYPESCRIPT
// composables/usePagination.ts
export function usePagination(totalItems: Ref<number>, pageSize = 20) {
  const route = useRoute()
  const router = useRouter()

  const currentPage = computed(() => Number(route.query.page) || 1)
  const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
  const hasNext = computed(() => currentPage.value < totalPages.value)
  const hasPrev = computed(() => currentPage.value > 1)

  function goToPage(page: number) {
    router.push({ query: { ...route.query, page: String(page) } })
  }

  function nextPage() {
    if (hasNext.value) goToPage(currentPage.value + 1)
  }

  function prevPage() {
    if (hasPrev.value) goToPage(currentPage.value - 1)
  }

  const offset = computed(() => (currentPage.value - 1) * pageSize)

  return { currentPage, totalPages, hasNext, hasPrev, offset, goToPage, nextPage, prevPage }
}
TYPESCRIPT
// composables/useNotification.ts
interface Notification { id: number; message: string; type: 'success' | 'error' | 'info' }

export function useNotification() {
  const notifications = useState<Notification[]>('notifications', () => [])

  function notify(message: string, type: Notification['type'] = 'success') {
    const id = Date.now()
    notifications.value = [...notifications.value, { id, message, type }]
    setTimeout(() => {
      notifications.value = notifications.value.filter(n => n.id !== id)
    }, 3000)
  }

  function dismiss(id: number) {
    notifications.value = notifications.value.filter(n => n.id !== id)
  }

  return { notifications, notify, dismiss }
}

❓Perguntas Frequentes

P Como escolher entre Composables e Pinia Store?
R Use Store para estado persistente entre componentes (ex.: carrinho de compras, dados do usuário), e use Composables para reutilizar lógica (ex.: formatação, busca). Composables são mais leves, enquanto Store oferece suporte a DevTools.
P Posso usar useFetch em um Composable?
R Sim, isso é chamado de Composable assíncrono. Você precisa usar await ao chamá-lo, assim como faria em uma página. useProductSearch é projetado dessa forma.
P Por que os Composables devem começar com "use"?
R O recurso de auto-import do Nuxt 3 apenas escaneia arquivos sob o diretório composables/ e não requer o prefixo "use". No entanto, a convenção da comunidade Vue é começá-los com "use" para identificação mais fácil e suporte de ferramentas.
P Posso usar useState no Composable?
R Sim. O estado criado por useState é hidratado automaticamente durante o SSR e no lado do cliente, tornando-o adequado para compartilhamento entre componentes. useWishlist usa useState para armazenamento.
P Os parâmetros de um Composable devem ser Refs ou valores brutos?
R Recomendamos aceitar Refs para que o Composable possa atualizar reativamente quando os parâmetros mudarem. Use unref() para suportar ambos os tipos.
P Múltiplos componentes que chamam o mesmo Composable compartilham estado?
R Se useState for usado dentro do Composable, o estado é compartilhado. Se ref ou reactive for usado, um estado separado é criado a cada vez que o Composable é chamado. A escolha depende das suas necessidades.

📖Resumo


📝Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Crie um Composable usePriceFormat para formatar preços como USD/JPY/CNY em componentes diferentes.
  2. Exercício Avançado (Dificuldade: ⭐⭐): Crie useProductSearch para implementar busca por palavras-chave, filtragem por categorias e paginação. Verifique se a página atualiza automaticamente quando o parâmetro watch muda.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um Composable composto useCheckout que integre o Store do carrinho de compras, formatação de preços e envio de pedidos para implementar um processo completo de checkout.

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%