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


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:

JS
// ❌ 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

JS
// 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 }
}
VUE
<!-- 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:


3. Noções básicas sobre composição

(1) Convenções de nomenclatura

JS
// ✅ 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

TEXT 📖 Somente leitura
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

JS
// 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 }
}
VUE
<!-- 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

JS
// 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
}
VUE
<!-- 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

JS
// 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 }
}
VUE
<!-- 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

JS
// 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)
    }
  }))
}
JS
// 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

JS
// 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 }
}
VUE
<!-- 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

JS
// 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

JS
// 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

JS
// 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.

BASH
# Installation
npm install @vueuse/core
JS
// 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

JS
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

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 }
}
▶ Experimente

▶ Exemplo: 2. Implementação completa do useFetch

JS
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 }
}
▶ Experimente

▶ Exemplo: 3. Implementação completa do 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 })
  
  // Cross-Tab Synchronization
  window.addEventListener('storage', (e) => {
    if (e.key === key && e.newValue) {
      data.value = JSON.parse(e.newValue)
    }
  })
  
  return data
}
▶ Experimente

Exemplo: 4. useDebounce + useThrottle

JS
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, useSearch pode conter useFetch + 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 em data.value. Use o Vitest para testes de unidade.


📖 Resumo


📝 Exercícios

  1. 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
  2. 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
  3. Problema desafiador (Dificuldade: ⭐⭐⭐)

    Implementar uma “biblioteca Composable de back-end para comércio eletrônico” completa:

    1. 5 Composables: useCart / useAuth / useSearch / usePagination / useTable
    2. Cada Composable deve ter pelo menos 50 linhas de código de implementação + testes completos
    3. Exportar tudo para composables/index.js
    4. Escreva 5 casos de teste (usando o Vitest)
    5. Compare sua implementação com a da biblioteca VueUse
    6. Documentar a API de cada Composable
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%