Composables Avançados no Vue 3: Padrões Reutilizáveis e Perguntas de Entrevista 2026
Domine composables avançados do Vue 3 com padrões reutilizáveis, tipagem TypeScript e perguntas frequentes de entrevistas técnicas. Guia completo com exemplos práticos de composição, validação de formulários, injeção de dependências e testes unitários.

A Composition API do Vue 3 transformou a maneira como desenvolvedores organizam e compartilham lógica entre componentes. No centro dessa transformação estão os composables — funções que encapsulam estado reativo e lógica reutilizável. Para entrevistas técnicas em 2026, entender composables não é apenas desejável: é um requisito fundamental para qualquer vaga que envolva Vue.js.
Este artigo explora padrões avançados de composables, desde a estrutura básica até composição entre composables, injeção de dependências com provide/inject, validação de formulários e testes unitários. Cada exemplo utiliza TypeScript e segue as convenções recomendadas pela documentação oficial do Vue 3.
Um composable é uma função que utiliza a Composition API do Vue para encapsular e reutilizar lógica com estado (stateful logic). Por convenção, o nome sempre começa com use — como useCounter, useFetch ou useAuth. Composables podem conter refs, computeds, watchers e lifecycle hooks, retornando valores reativos que os componentes consomem diretamente.
Anatomia de um Composable Bem Estruturado
Antes de explorar padrões avançados, é essencial compreender a estrutura que torna um composable robusto, tipado e fácil de manter. Um bom composable segue princípios claros: aceita opções via parâmetro, define interfaces explícitas para entrada e saída, e retorna valores reativos de forma previsível.
O exemplo a seguir demonstra um composable useCounter com limites configuráveis e um valor computado derivado:
import { ref, computed, type Ref } from 'vue'
interface UseCounterOptions {
min?: number
max?: number
initialValue?: number
}
interface UseCounterReturn {
count: Ref<number>
doubled: Ref<number>
increment: () => void
decrement: () => void
reset: () => void
}
export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
const { min = 0, max = Infinity, initialValue = 0 } = options
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
if (count.value < max) count.value++
}
function decrement() {
if (count.value > min) count.value--
}
function reset() {
count.value = initialValue
}
return { count, doubled, increment, decrement, reset }
}Vários aspectos tornam esse composable exemplar. A interface UseCounterOptions define parâmetros opcionais com valores padrão via desestruturação. A interface UseCounterReturn documenta exatamente o que o composable retorna, facilitando o autocompletar nas IDEs e a compreensão por outros desenvolvedores. O computed derivado (doubled) demonstra como composables podem expor valores calculados que se atualizam automaticamente quando o estado base muda.
Em entrevistas, é comum que o entrevistador peça para implementar um composable como esse do zero, avaliando o domínio sobre ref, computed e a convenção de nomenclatura.
Composables Assíncronos com Tratamento de Erros
Requisições HTTP são um dos casos de uso mais frequentes para composables. Um composable assíncrono bem construído precisa gerenciar três estados fundamentais: os dados retornados, o estado de carregamento e possíveis erros. Além disso, deve lidar com cancelamento de requisições para evitar race conditions.
import { ref, watchEffect, onUnmounted, toValue, type Ref, type MaybeRefOrGetter } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<string | null>
isLoading: Ref<boolean>
refresh: () => Promise<void>
}
export function useFetchData<T>(
url: MaybeRefOrGetter<string>
): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const isLoading = ref(false)
let abortController: AbortController | null = null
async function fetchData() {
// Cancel any in-flight request
abortController?.abort()
abortController = new AbortController()
isLoading.value = true
error.value = null
try {
const response = await fetch(toValue(url), {
signal: abortController.signal
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
error.value = err instanceof Error ? err.message : 'Unknown error'
} finally {
isLoading.value = false
}
}
// Re-fetch when URL changes reactively
watchEffect(() => {
fetchData()
})
onUnmounted(() => abortController?.abort())
return { data, error, isLoading, refresh: fetchData }
}Há vários detalhes importantes nessa implementação. O tipo MaybeRefOrGetter<string> permite que a URL seja passada como string simples, como ref ou como getter, oferecendo flexibilidade máxima ao consumidor. O AbortController cancela requisições anteriores quando a URL muda, evitando que respostas desatualizadas sobrescrevam dados mais recentes. O watchEffect dispara automaticamente uma nova requisição sempre que qualquer dependência reativa (neste caso, a URL) muda. O cleanup via onUnmounted garante que requisições em andamento sejam canceladas quando o componente é destruído.
Esse padrão é frequentemente solicitado em entrevistas técnicas, onde candidatos precisam demonstrar conhecimento sobre gerenciamento de estado assíncrono e prevenção de memory leaks.
Composables que utilizam onMounted, onUnmounted ou outros lifecycle hooks só funcionam quando chamados dentro do setup() de um componente (ou dentro de outro composable chamado em setup()). Chamar esses composables fora do ciclo de vida do componente — como em um callback assíncrono atrasado — gera erros silenciosos. Sempre garanta que a chamada ocorra de forma síncrona durante a inicialização do componente.
Composição de Composables
Um dos padrões mais poderosos da Composition API é a capacidade de compor composables entre si. Em vez de criar funções monolíticas, composables menores e focados podem ser combinados para resolver problemas complexos. Esse padrão de composição é análogo ao princípio de responsabilidade única (SRP) no design orientado a objetos.
O exemplo a seguir combina useFetchData com um useDebouncedRef para criar uma busca paginada reativa:
import { ref, computed, watch, type Ref } from 'vue'
import { useFetchData } from './useFetchData'
import { useDebouncedRef } from './useDebouncedRef'
interface UsePaginatedSearchReturn<T> {
query: Ref<string>
page: Ref<number>
results: Ref<T[] | null>
totalPages: Ref<number>
isLoading: Ref<boolean>
error: Ref<string | null>
nextPage: () => void
prevPage: () => void
}
export function usePaginatedSearch<T>(
baseUrl: string,
perPage = 20
): UsePaginatedSearchReturn<T> {
const query = useDebouncedRef('', 300)
const page = ref(1)
const totalPages = ref(1)
const apiUrl = computed(
() => `${baseUrl}?q=${encodeURIComponent(query.value)}&page=${page.value}&limit=${perPage}`
)
const { data, error, isLoading } = useFetchData<{ items: T[]; total: number }>(apiUrl)
const results = computed(() => data.value?.items ?? null)
watch(data, (response) => {
if (response) {
totalPages.value = Math.ceil(response.total / perPage)
}
})
// Reset to page 1 when query changes
watch(query, () => { page.value = 1 })
function nextPage() {
if (page.value < totalPages.value) page.value++
}
function prevPage() {
if (page.value > 1) page.value--
}
return { query, page, results, totalPages, isLoading, error, nextPage, prevPage }
}Esse composable demonstra a composição em ação. O useDebouncedRef gerencia o debounce da digitação, evitando requisições excessivas enquanto o usuário digita. O useFetchData cuida de toda a lógica de fetch, cancelamento e estados de loading/erro. O usePaginatedSearch orquestra esses composables menores, adicionando lógica de paginação e reset automático ao trocar a query.
Essa abordagem modular facilita os testes (cada composable pode ser testado isoladamente), melhora a legibilidade e permite que cada peça seja reutilizada em outros contextos da aplicação.
Pronto para mandar bem nas entrevistas de Vue.js / Nuxt.js?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Injeção de Dependências com provide/inject
O mecanismo provide/inject do Vue permite que composables compartilhem estado entre componentes sem prop drilling. Esse padrão é particularmente útil para dados que precisam estar disponíveis em toda uma subárvore de componentes, como temas, configurações de internacionalização ou estado de autenticação.
import { provide, inject, ref, readonly, type InjectionKey, type Ref } from 'vue'
type Theme = 'light' | 'dark' | 'system'
interface ThemeContext {
theme: Readonly<Ref<Theme>>
setTheme: (t: Theme) => void
resolvedTheme: Readonly<Ref<'light' | 'dark'>>
}
const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export function provideTheme(initial: Theme = 'system') {
const theme = ref<Theme>(initial)
const resolvedTheme = computed<'light' | 'dark'>(() => {
if (theme.value !== 'system') return theme.value
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
})
function setTheme(t: Theme) {
theme.value = t
}
const context: ThemeContext = {
theme: readonly(theme),
setTheme,
resolvedTheme: readonly(resolvedTheme)
}
provide(ThemeKey, context)
return context
}
export function useTheme(): ThemeContext {
const context = inject(ThemeKey)
if (!context) {
throw new Error('useTheme() requires a parent component to call provideTheme()')
}
return context
}Vários padrões importantes aparecem nessa implementação. A InjectionKey tipada com generics garante type-safety total entre o provider e o consumer, eliminando a necessidade de type assertions manuais. O readonly() protege o estado de modificações diretas, forçando consumidores a usar setTheme() para alterar o tema. A separação entre provideTheme (chamado no componente raiz) e useTheme (chamado nos componentes filhos) segue a convenção do Vue para composables que dependem de contexto hierárquico.
Esse padrão também é relevante para perguntas de entrevista sobre Vue, onde candidatos precisam explicar a diferença entre provide/inject e stores globais como Pinia, e quando cada abordagem é mais adequada.
Composable de Validação de Formulários
Formulários são onipresentes em aplicações web, e a validação é um dos maiores pontos de complexidade. Um composable de validação permite centralizar regras, gerenciar mensagens de erro e verificar o estado de validade do formulário de forma reativa.
import { reactive, computed, type UnwrapNestedRefs } from 'vue'
type ValidationRule<T> = (value: T) => string | true
type FieldRules<T> = { [K in keyof T]?: ValidationRule<T[K]>[] }
interface UseFormReturn<T extends Record<string, any>> {
fields: UnwrapNestedRefs<T>
errors: Record<keyof T, string>
isValid: Ref<boolean>
validate: () => boolean
resetErrors: () => void
}
export function useFormValidation<T extends Record<string, any>>(
initialValues: T,
rules: FieldRules<T>
): UseFormReturn<T> {
const fields = reactive({ ...initialValues }) as UnwrapNestedRefs<T>
const errors = reactive(
Object.keys(initialValues).reduce(
(acc, key) => ({ ...acc, [key]: '' }),
{} as Record<keyof T, string>
)
)
function validate(): boolean {
let valid = true
for (const key of Object.keys(rules) as (keyof T)[]) {
const fieldRules = rules[key] || []
errors[key] = '' as any
for (const rule of fieldRules) {
const result = rule(fields[key])
if (result !== true) {
errors[key] = result as any
valid = false
break // Stop at first error per field
}
}
}
return valid
}
function resetErrors() {
for (const key of Object.keys(errors)) {
(errors as any)[key] = ''
}
}
const isValid = computed(() =>
Object.values(errors).every((e) => e === '')
)
return { fields, errors, isValid, validate, resetErrors }
}Esse composable utiliza reactive em vez de ref para gerenciar os campos e erros do formulário, o que permite acesso direto às propriedades sem .value. O sistema de regras é flexível: cada campo pode ter múltiplas funções de validação que retornam true para sucesso ou uma string de erro. A função validate() para no primeiro erro de cada campo (padrão fail-fast), enquanto o computed isValid reflete automaticamente o estado global de validade.
Em entrevistas técnicas, a capacidade de projetar um sistema de validação genérico e tipado demonstra domínio avançado tanto de Vue quanto de TypeScript.
Testando Composables
Composables precisam ser testados fora de componentes reais. Como eles dependem do contexto de setup() do Vue, é necessário criar um wrapper que forneça esse contexto durante os testes. O padrão withSetup resolve esse problema de forma elegante:
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useCounter } from './useCounter'
function withSetup<T>(composable: () => T): { result: T; unmount: () => void } {
let result!: T
const wrapper = mount(
defineComponent({
setup() {
result = composable()
return () => h('div')
}
})
)
return { result, unmount: () => wrapper.unmount() }
}
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = withSetup(() => useCounter())
expect(result.count.value).toBe(0)
})
it('respects min and max boundaries', () => {
const { result } = withSetup(() =>
useCounter({ min: 0, max: 3, initialValue: 3 })
)
result.increment()
expect(result.count.value).toBe(3) // Capped at max
result.count.value = 0
result.decrement()
expect(result.count.value).toBe(0) // Capped at min
})
it('computes doubled value reactively', () => {
const { result } = withSetup(() => useCounter({ initialValue: 5 }))
expect(result.doubled.value).toBe(10)
result.increment()
expect(result.doubled.value).toBe(12)
})
})A função withSetup cria um componente mínimo que executa o composable dentro do contexto adequado. Esse padrão é amplamente utilizado pela comunidade Vue e pela própria equipe do Vue Test Utils. Os testes verificam o valor inicial, os limites de incremento/decremento e a reatividade do valor computado.
Para composables que utilizam onMounted ou onUnmounted, o método unmount() retornado pelo wrapper permite simular a destruição do componente e verificar que os cleanups ocorrem corretamente.
O projeto VueUse (vueuse.org) oferece mais de 200 composables prontos para produção, cobrindo desde sensores do navegador até integrações com WebSocket e gerenciamento de estado. Estudar o código-fonte do VueUse é uma das melhores formas de aprender padrões avançados de composables. Muitos entrevistadores esperam que candidatos conheçam essa biblioteca e saibam quando utilizar composables prontos versus criar soluções customizadas.
Perguntas Comuns em Entrevistas
As perguntas a seguir aparecem frequentemente em entrevistas técnicas para vagas Vue.js em 2026. Cada resposta reflete o nível de profundidade esperado em processos seletivos para desenvolvedores pleno e sênior.
Qual é a diferença entre um composable e um mixin? Mixins (da Options API) sofrem de conflitos de namespace, fontes implícitas de propriedades e dificuldade de tipagem com TypeScript. Composables resolvem todos esses problemas: não há conflitos porque as variáveis são explicitamente importadas, a rastreabilidade é total porque o consumidor vê exatamente o que o composable retorna, e o TypeScript consegue inferir tipos automaticamente. Mixins são considerados um anti-pattern no Vue 3.
Quando usar composables versus Pinia? Composables são ideais para lógica reutilizável associada a componentes individuais ou subárvores de componentes (como validação de formulários, controles de UI ou requisições HTTP). Pinia é mais adequado para estado global da aplicação que precisa ser acessado por componentes não relacionados na árvore (como dados do usuário logado, carrinho de compras ou configurações da aplicação). Na prática, composables e Pinia frequentemente trabalham juntos — uma store Pinia pode usar composables internamente.
Como evitar memory leaks em composables?
Os principais cuidados são: cancelar requisições HTTP em andamento no onUnmounted (usando AbortController), remover event listeners registrados no window ou document, limpar intervalos e timeouts com clearInterval/clearTimeout, e descartar conexões WebSocket. O padrão recomendado é registrar o cleanup no mesmo local onde o efeito colateral é criado.
O que é MaybeRefOrGetter e por que importa?
É um tipo utilitário do Vue 3.3+ que aceita um valor como Ref<T>, () => T ou T diretamente. Isso torna composables mais flexíveis, pois o consumidor pode passar valores de diferentes formas sem que o composable precise saber qual formato foi usado. A função toValue() unifica a leitura independentemente do formato recebido.
Como testar composables que dependem de lifecycle hooks?
Composables com onMounted, onUnmounted e similares precisam ser executados dentro de um contexto de setup(). O padrão withSetup — que cria um componente wrapper mínimo usando mount() do Vue Test Utils — é a abordagem recomendada. Para composables que dependem de provide/inject, o mount() aceita uma opção global.provide para injetar as dependências necessárias durante o teste.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Conclusão
Composables representam a abordagem moderna e recomendada para organizar lógica reutilizável no Vue 3. Dominar os padrões apresentados neste artigo prepara desenvolvedores tanto para projetos reais quanto para entrevistas técnicas cada vez mais exigentes.
Os pontos centrais abordados incluem:
- Estrutura tipada: interfaces explícitas para opções e retorno garantem autocompletar e previnem erros em tempo de compilação
- Composables assíncronos: gerenciamento completo de loading, erro e cancelamento via
AbortController - Composição: composables menores e focados combinados para resolver problemas complexos, seguindo o princípio de responsabilidade única
- provide/inject: injeção de dependências tipada para compartilhar estado sem prop drilling
- Validação de formulários: sistema genérico e extensível com regras customizáveis por campo
- Testes: padrão
withSetuppara executar composables em contexto adequado com Vitest e Vue Test Utils - VueUse: biblioteca de referência com mais de 200 composables prontos para produção
Para quem se prepara para entrevistas Vue.js em 2026, a recomendação é implementar cada um desses padrões do zero, compreendendo não apenas o como, mas o porquê de cada decisão de design.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Vue 3 Pinia vs Vuex: Gerenciamento de Estado Moderno e Perguntas de Entrevista 2026
Comparação detalhada entre Pinia e Vuex: design de API, suporte TypeScript, Composition API, desempenho, estratégias de migração e perguntas frequentes em entrevistas Vue para 2026.

Nuxt 4 em 2026: nova estrutura de diretórios e migração do Nuxt 3
Guia completo de migração para o Nuxt 4: estrutura de diretórios app/, camada singleton de busca de dados, melhorias no TypeScript e instruções passo a passo com exemplos de código.

Perguntas essenciais de Vue.js: 25 perguntas para conquistar a vaga
Prepare-se para entrevistas de Vue.js com estas 25 perguntas essenciais. Da reatividade aos composables, domine os conceitos-chave para a próxima entrevista.