Capítulo 08: Criação de Composables Reutilizáveis (Custom Vue Hooks)
Capítulo 08: Criação de Composables Reutilizáveis (Custom Vue Hooks)
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual da Arquitetura de Composables
flowchart TD
A["Regra de Negócio / Estado / Efeito"] --> B["Custom Composable: useMinhaLogica()"]
B --> C["Estado Reativo Interno (ref / reactive)"]
B --> D["Derivações e Cálculos (computed)"]
B --> E["Listeners / Timers / I/O Externo"]
E --> F["onScopeDispose: Limpeza Automática"]
C & D --> G["Retorno de Objeto de Refs Desestruturável"]
G --> H["Componente A (UI)"]
G --> I["Componente B (UI)"]
G --> J["Store do Pinia / EffectScope"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style F fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. O Padrão Canônico de Composables no Vue 3
Um Composable (equivalente funcional aos Custom Hooks do React) é uma função JavaScript que encapsula e reutiliza lógica de estado com recursos da Composition API do Vue. Ao contrário dos legados Mixins do Vue 2, os composables possuem origem 100% transparente de dados, não sofrem com colisões de nomes e integram-se perfeitamente com o compilador do TypeScript.
1.1 Convenções de Nomenclatura e Retorno
- Prefixo
use...: Toda função composable deve iniciar com o termouse, denotando que faz uso do ciclo de reatividade (ex:useUserSession,useWindowScroll,useDebounce). - Retorno de Objeto com Refs: Sempre retorne um objeto JavaScript regular contendo propriedades do tipo
Ref<T>individuais, e não um objeto reativo fechado. Isso permite que os componentes consumidores utilizem desestruturação livre (const { data, isLoading } = useMinhaApi()) sem perder a reatividade de cada variável.
1.2 Injeção de Dependência Tipada: provide() e inject()
Para passar configurações ou instâncias através de árvores profundas de componentes sem sofrer com Prop Drilling, utiliza-se o mecanismo de injeção de dependência do Vue. Para garantir segurança de tipos em TypeScript, deve-se utilizar InjectionKey<T>:
// types/keys.ts
import type { InjectionKey, Ref } from 'vue';
export const UserSessionKey: InjectionKey<Ref<UserSession>> = Symbol('UserSessionKey');
// Componente Raiz:
provide(UserSessionKey, sessionRef);
// Componente Profundo / Composable:
const session = inject(UserSessionKey);
if (!session) throw new Error('UserSessionKey não provido no contexto ancestral.');
1.3 onScopeDispose e Descarte de Recursos
Em composables robustos, utiliza-se onScopeDispose no lugar de onUnmounted. Enquanto onUnmounted só funciona quando o composable é chamado diretamente dentro do ciclo de vida de um componente SFC, o onScopeDispose é capaz de limpar listeners e timers mesmo quando o composable é instanciado dentro de stores do Pinia, testes do Vitest ou blocos isolados com effectScope().
⚖️ 2. Tabela Comparativa: Mixins (Vue 2) vs Composables (Vue 3)
| Critério de Avaliação | Mixins Legados (Vue 2) | Composables Modernos (Vue 3) |
|---|---|---|
| Origem das Propriedades | Implícita e opaca; difícil saber de qual mixin veio uma variável. | Explícita; os valores retornados são desestruturados no script. |
| Colisão de Nomes | Frequente; dois mixins com a mesma chave sobrescrevem um ao outro. | Impossível; variáveis podem ser renomeadas na desestruturação. |
| Integração com TypeScript | Praticamente inviável sem any ou casting manual. |
Total e Automática; contratos de retorno inferidos estaticamente. |
| Suporte a Parâmetros | Não suporta passagem de argumentos na inicialização. | Suporta múltiplos parâmetros e opções dinâmicas. |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Chamar Composables de Forma Assíncrona ou Condicional
Erro Comum:
// ❌ ANTI-PATTERN GRAVE:
async function load() {
await fetchConfig();
const { isOnline } = useNetwork(); // ERRO: Chamado fora da execução síncrona de setup()!
}
Regra Fundamental: Composables que registram hooks de ciclo de vida (onMounted, onScopeDispose) ou leem contexto (inject) devem ser invocados sincronamente no corpo principal de <script setup>.
Anti-Pattern 2: Retornar Objeto reactive Diretamente
Erro Comum: Retornar return reactive({ x, y }). Se o consumidor fizer const { x } = useMouse(), x perde o vínculo com a reatividade do Proxy.
Abordagem Correta: Retorne return { x, y } onde x e y são instâncias de ref(), ou aplique toRefs() no retorno.
💻 4. Código Conceitual Arquitetural Comentado
// src/composables/useWindowScrollPosition.ts
import { ref, onScopeDispose, type Ref } from 'vue';
export interface ScrollPosition {
x: Ref<number>;
y: Ref<number>;
isScrolledDown: Ref<boolean>;
}
export function useWindowScrollPosition(thresholdPx = 100): ScrollPosition {
const x = ref<number>(0);
const y = ref<number>(0);
const isScrolledDown = ref<boolean>(false);
// Verificação defensiva de ambiente para suporte pleno ao SSR / Nuxt 3
if (typeof window !== 'undefined') {
const updateScroll = () => {
x.value = window.scrollX;
y.value = window.scrollY;
isScrolledDown.value = window.scrollY > thresholdPx;
};
window.addEventListener('scroll', updateScroll, { passive: true });
updateScroll(); // Captura a posição inicial
// onScopeDispose: Limpa o listener automaticamente ao destruir o escopo reativo
onScopeDispose(() => {
console.log('[useWindowScrollPosition] Listener de scroll descartado.');
window.removeEventListener('scroll', updateScroll);
});
}
return { x, y, isScrolledDown };
}
🛠️ 5. Cheat-Sheet de Boas Práticas para Composables
// 1. Definição com generics e tipagem estrita
export function useFetchData<T>(url: string | Ref<string>) {
const data = ref<T | null>(null);
const isPending = ref(false);
// Aceita tanto string estática quanto Ref reativa (toValue / unref)
const targetUrl = computed(() => toValue(url));
// 2. Retorno desestruturável
return { data, isPending };
}
// 3. Consumo no componente
const { data, isPending } = useFetchData<Usuario[]>('/api/usuarios');