Vue.js: Composables Personalizados
Última atualização: 2026-08-26
As funções composíveis (também conhecidas como hooks) são o padrão central de reutilização da API de Composição do Vue 3 — elas encapsulam “dados reativos + lógica de negócios” em funções reutilizáveis. Essencialmente, são “pacotes de lógica” compostos por ref, computed, watch e assim por diante.
Dominar os Composables é fundamental para escrever aplicativos Vue fáceis de manter — eles tornam o código dos componentes mais conciso e permitem que a lógica de negócios seja compartilhada entre os componentes. Esta lição vai te ajudar a criar sua própria biblioteca de Composables do zero.
1. O que você vai aprender
- O que é um Composable (hook) e por que ele é necessário?
- Convenções de nomenclatura modulares
useXxx - 5 Composables práticos (useMouse / useLocalStorage / useFetch / useDebounce / useToggle)
- Melhores práticas para receber parâmetros e retornar valores
- Encapsulamento de ganchos de ciclo de vida
- Composables compartilhados entre componentes (diretório composables/)
- Biblioteca VueUse (mais de 30 Composables prontos para uso)
2. A lógica “Get + debounce” para um carrinho de compras foi duplicada 5 vezes
(1) Problema: 5 campos de pesquisa, 5 instâncias de código duplicado
O painel de administração da Alice tinha 5 campos de pesquisa (pedidos/produtos/usuários/etc.), cada um com a lógica de fetch + debounce:
// ❌ The "Broken" Version:5 component,5 Duplicate code
// OrdersSearch.vue
let timer = null
const searchQuery = ref('')
const results = ref([])
watch(searchQuery, (val) => {
if (timer) clearTimeout(timer)
timer = setTimeout(async () => {
const res = await fetch(`/api/orders?q=${val}`)
results.value = await res.json()
}, 300)
})
// ProductsSearch.vue - Exactly the same logic(Change only URL)
// UsersSearch.vue - Exactly the same logic(Change only URL)
// ... And also 2 more ...
Total: 50 linhas × 5 = 250 linhas de código duplicado. A correção do bug exige alterações em 5 lugares.
O gerente de produto Charlie acrescenta um novo requisito:
“Alice, altere o tempo de espera entre teclas de 300 ms para 500 ms. E adicione uma verificação de comprimento mínimo (só pesquisar se houver 3 ou mais caracteres).”
Alice precisa editar 5 arquivos. Isso pode gerar erros.
(2) Solução com Vue Composable: 1 useSearch, reutilizada em 5 lugares
// composables/useSearch.js - 1 Composable
import { ref, watch } from 'vue'
export function useSearch(apiUrl, options = {}) {
const { debounceMs = 300, minLength = 0 } = options
const searchQuery = ref('')
const results = ref([])
const loading = ref(false)
let timer = null
watch(searchQuery, (val) => {
if (timer) clearTimeout(timer)
if (val.length < minLength) {
results.value = []
return
}
timer = setTimeout(async () => {
loading.value = true
const res = await fetch(`${apiUrl}?q=${val}`)
results.value = await res.json()
loading.value = false
}, debounceMs)
})
return { searchQuery, results, loading }
}
<!-- OrdersSearch.vue - 1 Line call -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/orders', { debounceMs: 500, minLength: 3 })
</script>
<!-- ProductsSearch.vue - Likewise 1 line (Different URL) -->
<script setup>
import { useSearch } from '@/composables/useSearch'
const { searchQuery, results, loading } = useSearch('/api/products', { debounceMs: 500, minLength: 3 })
</script>
1 função useSearch → reutilizada em 5 lugares. Alterar o debounce em um lugar faz com que a mudança tenha efeito em todos os lugares.
(3) Receita
Após o Composable:
- Tamanho do código: 250 → 60 linhas (-76%)
- 1 alteração necessária para a implementação: 1 arquivo Composable
- Nova caixa de pesquisa: chamada de 1 linha
- Testável: o useSearch pode ser testado em unidade de forma independente
3. Noções básicas sobre composição
(1) Convenções de nomenclatura
// ✅ Correct:useXxx Naming
useMouse()
useLocalStorage('key')
useFetch('/api/users')
useDebounce(searchQuery, 500)
useToggle(false)
// ❌ Error:Other Names
fetchUser() // Not based on use Introduction
mouseTracker() // Not based on use Introduction
getStorage() // Verb get/set Not included hook
(2) 5 características principais
| Recurso | Descrição |
|---|---|
| Começa com “use” | Padrão do setor (o React também usa “useXxx”) |
| Retornar dados reativos | Retornar ref / computado / reativo |
| Parâmetros aceitos | Normalmente aceita tipos primitivos (string/número/objeto) |
| Pode ser usado de forma independente | Basta inserir useXxx() no componente |
| Composable | Vários Composables podem ser aninhados |
(3) Organização de arquivos
src/
├── components/ # Components
├── views/ # Page
├── composables/ # Composable Function(Key Points)
│ ├── useMouse.js
│ ├── useLocalStorage.js
│ ├── useFetch.js
│ ├── useDebounce.js
│ ├── useToggle.js
│ └── index.js # Batch Export
├── stores/ # Pinia stores
└── App.vue
4. 5 exemplos práticos de composição
(1) useMouse: Rastrear a posição do mouse
// composables/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.clientX
y.value = event.clientY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
<!-- MouseTracker.vue -->
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Mouse: {{ x }}, {{ y }}</p>
</template>
(2) useLocalStorage: localStorage reativo
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
return data
}
<!-- ThemeToggle.vue -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
Current: {{ theme }}
</button>
</template>
(3) useFetch: Recuperação de dados para fins gerais
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const res = await fetch(url.value || url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// Get It Now
fetchData()
return { data, loading, error, refetch: fetchData }
}
<!-- UserList.vue -->
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: users, loading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="refetch">Refresh</button>
</template>
(4) useDebounce: Valor de debounce
// composables/useDebounce.js
import { ref, customRef } from 'vue'
export function useDebounce(value, delay = 300) {
let timer = null
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value.value = newValue
trigger()
}, delay)
}
}))
}
// Usage
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)
watch(debouncedSearch, (val) => {
console.log('Search:', val)
// 500ms Execute later
})
(5) useToggle: botão de alternância
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initialValue = false) {
const value = ref(initialValue)
function toggle() {
value.value = !value.value
}
function setTrue() {
value.value = true
}
function setFalse() {
value.value = false
}
return { value, toggle, setTrue, setFalse }
}
<!-- ModalToggle.vue -->
<script setup>
import { useToggle } from '@/composables/useToggle'
const { value: showModal, open: setTrue, close: setFalse } = useToggle(false)
</script>
<template>
<button @click="setTrue">Open Modal</button>
<Modal v-if="showModal" @close="setFalse" />
</template>
5. Melhores práticas para composição
(1) Definição dos 6 parâmetros-chave
// 1. Basic Parameters
useDebounce(value, 300)
// 2. Configuration Object
useFetch(url, { method: 'POST', body: data })
// 3. Citation Types(Responsive)
useFetch(ref('/api/users'))
// 4. Function Arguments(callback)
useEventListener('click', (e) => console.log(e))
// 5. Multi-parameter
useLocalStorage('key', defaultValue, { mergeDefaults: true })
// 6. Generics(TypeScript)
useLocalStorage<User>('user', { name: '', age: 0 })
(2) 5 considerações fundamentais para o projeto do valor de retorno
// 1. Return directly ref
export function useCounter() {
const count = ref(0)
return { count }
}
// 2. Back ref + Methods
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// 3. Return to Namespace(Recommendations)
export function useCounter() {
const count = ref(0)
return {
state: { count },
actions: { increment: () => count.value++ }
}
}
// 4. Back ref(Deconstructing Friendship)
export function useCounter() {
return useRef(0) // Return directly ref,For external use .value
}
// 5. Back readonly(Prevent External Modifications)
import { readonly } from 'vue'
export function useCounter() {
const count = ref(0)
return { count: readonly(count) }
}
(3) Seis principais encapsulamentos do ciclo de vida
// 1. onMounted + onUnmounted(Most common)
export function useEventListener(event, handler) {
onMounted(() => window.addEventListener(event, handler))
onUnmounted(() => window.removeEventListener(event, handler))
}
// 2. watch Automatic Cleanup
export function useWatch(source, callback) {
const stop = watch(source, callback)
onUnmounted(() => stop())
}
// 3. setInterval Automatic Cleanup
export function useInterval(fn, delay) {
let timer = null
onMounted(() => { timer = setInterval(fn, delay) })
onUnmounted(() => clearInterval(timer))
}
// 4. setTimeout Automatic Cleanup
export function useTimeout(fn, delay) {
let timer = null
onMounted(() => { timer = setTimeout(fn, delay) })
onUnmounted(() => clearTimeout(timer))
}
// 5. Canceling Asynchronous Tasks
export function useAsyncTask(task) {
let cancelled = false
onUnmounted(() => { cancelled = true })
return async () => {
if (cancelled) return
await task()
}
}
// 6. Route Redirect Cleanup
export function useRouteLeave(callback) {
onBeforeRouteLeave((to, from) => {
if (callback()) return false // Prevent Departure
})
}
6. Biblioteca VueUse (mais de 30 composáveis)
(1) O que é o VueUse?
O VueUse é uma biblioteca de Composables da comunidade Vue que oferece mais de 200 Composables prontos para uso (como useMouse, useLocalStorage, useDebounce e useEventListener), poupando-lhe o trabalho de escrevê-los você mesmo.
# Installation
npm install @vueuse/core
// main.js
import { createApp } from 'vue'
import VueUse from '@vueuse/core'
import App from './App.vue'
createApp(App).use(VueUse).mount('#app')
(2) Os 5 composáveis mais usados do VueUse
import { useMouse, useLocalStorage, useDebounceFn, useEventListener, useToggle } from '@vueuse/core'
// 1. Mouse Position
const { x, y } = useMouse()
// 2. localStorage Responsive
const theme = useLocalStorage('theme', 'light')
// 3. Function Debouncing
const debouncedFn = useDebounceFn(() => {
console.log('Debounced!')
}, 500)
// 4. Global Event Listeners
useEventListener('resize', () => {
console.log('Window resized')
})
// 5. Switch Toggle
const [value, toggle] = useToggle()
(3) 5 principais casos de uso
| Cenário | VueUse Composable |
|---|---|
| Posição do mouse | useMouse / useMouseInElement |
| Posição da rolagem | useScroll / useInfiniteScroll |
| localStorage | useLocalStorage / useStorage |
| Status da rede | useOnline / useNetwork |
| Consultas de mídia | useMediaQuery / useBreakpoints |
| Tela cheia | usar tela cheia |
| Área de transferência | useClipboard |
| Arrastar com o mouse | useDraggable |
| Tamanho do elemento | useElementSize / useResizeObserver |
| Supressão de rebote / Limitação | useDebounceFn / useThrottleFn |
7. Exemplo completo: uma combinação de 5 composáveis
▶ Exemplo: 1. Implementação completa de useMouse
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
▶ Exemplo: 2. Implementação completa do useFetch
import { ref, watch, isRef } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
const requestUrl = isRef(url) ? url.value : url
if (!requestUrl) return
loading.value = true
error.value = null
try {
const res = await fetch(requestUrl, options)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
watch(url, fetchData, { immediate: true })
return { data, loading, error, refetch: fetchData }
}
▶ Exemplo: 3. Implementação completa do useLocalStorage
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored !== null ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
// Cross-Tab Synchronization
window.addEventListener('storage', (e) => {
if (e.key === key && e.newValue) {
data.value = JSON.parse(e.newValue)
}
})
return data
}
Exemplo: 4. useDebounce + useThrottle
import { ref, customRef } from 'vue'
// Image Stabilization:After the last operation delay Trigger
export function useDebounce(value, delay = 300) {
let timer = null
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value.value = newValue
trigger()
}, delay)
}
}))
}
// Cost-cutting: Max 1 trigger during delay period
export function useThrottle(value, delay = 300) {
let last = 0
return customRef((track, trigger) => ({
get() {
track()
return value.value
},
set(newValue) {
const now = Date.now()
if (now - last >= delay) {
last = now
value.value = newValue
trigger()
}
}
}))
}
▶ Exemplo: 5. Referência rápida para 5 erros comuns
| Erro | Sintoma | Solução |
|---|---|---|
| Não começa com “use” | Não é reconhecido pela equipe | Tem o nome “useXxx” |
| Retornar um objeto comum | Perder a reatividade | Retornar uma referência |
| Efeitos colaterais da falta de limpeza | Vazamentos de memória | Limpeza no método onUnmounted |
| Abstração excessiva | Use Composables para cenários simples | Coloque a lógica simples diretamente nos componentes |
| Composable aninhado em 5 níveis | Difícil de depurar | Divida-o em subcomponentes ou use o Pinia |
▶ Exemplo: 6. Comparação de desempenho dos 5 principais Composables
| Padrão | Reutilização | Desempenho | Manutenção | Aplicabilidade |
|---|---|---|---|---|
| Copiar e colar | ❌ | ⭐⭐⭐ | ❌ | Única vez |
| Mixin (Vue 2) | ⭐⭐ | ⭐⭐ | ⭐⭐ | Projeto legado |
| Composable | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Recomendado |
| Pinia | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Aplicativo de grande porte |
| Barramento de eventos | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | Comunicação entre componentes |
❓ Perguntas Frequentes
P: Qual é a diferença entre um Composable e um mixin? R: Um mixin é a abordagem de reutilização utilizada no Vue 2 (fusão implícita, conflitos de nomes). Um Composable é a abordagem utilizada no Vue 3 (retorno explícito, segurança de tipos). O Vue 3 recomenda o uso de Composables.
P: Um Composable precisa ser chamado no nível superior do
setup? R: Sim. Como os Composables utilizam refs, watches e hooks de ciclo de vida, eles devem ser chamados no nível superior do<script setup>.
P: Os Composables aceitam props? R: Eles não aceitam props diretamente. No entanto, podem aceitar refs (reativos) ou valores simples. A biblioteca VueUse faz uso extensivo de parâmetros ref.
P: Os Composables podem chamar uns aos outros? R: Sim. Os Composables são, essencialmente, funções e podem ser aninhados. Por exemplo,
useSearchpode conteruseFetch+useDebounce.
P: Quando devo escrever meu próprio código e quando devo usar o VueUse? R: Escreva seu próprio código para lógicas específicas do negócio (como useSearch ou useCart). Use o VueUse para lógicas de uso geral (eventos do mouse, rolagem, supressão de rebote, modo de tela cheia) para economizar 80% do seu tempo.
P: O Composable é uma cópia dos React Hooks? R: Sim, ele foi inspirado nos React Hooks. O Vue 3 se baseia neles e os aprimora: ele usa refs (coleta automática de dependências) combinadas com uma chamada de configuração de nível superior (sem a necessidade de envolver com useEffect).
P: Como faço para testar um Composable? R: Um Composable é uma função normal de JavaScript. Chame-o diretamente:
const { data } = useFetch('/api'), e faça uma asserção emdata.value. Use o Vitest para testes de unidade.
📖 Resumo
- Um Composable (hook) é uma função reutilizável composta por ref, computed e watch.
- 5 Características principais: Começa com “use” / Retorna uma resposta reativa / Aceita parâmetros / Pode ser usado de forma independente / É composível
- 5 Prático: useMouse / useLocalStorage / useFetch / useDebounce / useToggle
- 6 principais modelos de parâmetros + 5 principais modelos de valores de retorno
- 6 principais ganchos do ciclo de vida (onMounted/onUnmounted)
- A biblioteca VueUse oferece mais de 200 Composables
- Composable x Pinia: use o Composable para lógica simples e o Pinia para gerenciamento de estado em grande escala
📝 Exercícios
-
Basic Questions (Difficulty: ⭐)
Implemente a função
useToggle:- Aceitar valores iniciais (padrão: falso)
- Retorna { valor, toggle, setTrue, setFalse }
- Teste os 4 cenários de uso do componente
-
Problemas avançados (Dificuldade: ⭐⭐)
Implementação da versão completa do
useFetch:- Suporta o parâmetro
ref(URLs responsivas) - Suporta o método
refetch - Oferece suporte à atualização automática no relógio
- Tratamento de erros + Status de carregamento
- Testes em dois componentes
- Suporta o parâmetro
-
Problema desafiador (Dificuldade: ⭐⭐⭐)
Implementar uma “biblioteca Composable de back-end para comércio eletrônico” completa:
- 5 Composables: useCart / useAuth / useSearch / usePagination / useTable
- Cada Composable deve ter pelo menos 50 linhas de código de implementação + testes completos
- Exportar tudo para
composables/index.js - Escreva 5 casos de teste (usando o Vitest)
- Compare sua implementação com a da biblioteca VueUse
- Documentar a API de cada Composable