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
- Uma Visão Geral Completa dos Composables Built-in do Nuxt 3
- Composables Personalizados: Convenções de Diretório e Diretrizes de Nomenclatura
- Padrão de Design de Composable: Assíncrono / Reatividade de Parâmetros / Desestruturação de Valor de Retorno
- Composição de Composables: Um Composable chama outro Composable
- MegaShop: useProductSearch/usePriceFormat/useWishlist
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:
// 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
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
<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:
// 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
// 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:
// Execução Bem-sucedida
(2) ▶Exemplo: Favoritos com useWishlist
// 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:
// Execução Bem-sucedida
(3) ▶Exemplo: Lógica de busca com useProductSearch
// 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:
// 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
// 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:
// 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
// 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 }
}
// 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
useFetch em um Composable?await ao chamá-lo, assim como faria em uma página. useProductSearch é projetado dessa forma.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.useState no Composable?useState é hidratado automaticamente durante o SSR e no lado do cliente, tornando-o adequado para compartilhamento entre componentes. useWishlist usa useState para armazenamento.unref() para suportar ambos os tipos.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
- O Nuxt 3 inclui mais de 20 Composables built-in: useFetch, useState, useCookie, useRouter e mais
- Coloque Composables personalizados no diretório
composables/, nomeie-os com o prefixouse, e eles serão importados automaticamente. - Padrões de Design: Síncrono (formatação), Assíncrono (recuperação de dados), Reativo (busca), Composto (checkout)
- Um Composable pode chamar outros Composables para implementar lógica complexa em camadas
- MegaShop constrói sua camada de lógica de negócios usando usePriceFormat, useWishlist, useProductSearch e useCheckout
📝Exercícios
- Exercício Básico (Dificuldade: ⭐): Crie um Composable
usePriceFormatpara formatar preços como USD/JPY/CNY em componentes diferentes. - Exercício Avançado (Dificuldade: ⭐⭐): Crie
useProductSearchpara implementar busca por palavras-chave, filtragem por categorias e paginação. Verifique se a página atualiza automaticamente quando o parâmetrowatchmuda. - Desafio (Dificuldade: ⭐⭐⭐): Crie um Composable composto
useCheckoutque integre o Store do carrinho de compras, formatação de preços e envio de pedidos para implementar um processo completo de checkout.
---|



