Padrões e Melhores Práticas Vue.js
Guia abrangente para desenvolvimento Vue.js 3 usando Composition API (<script setup>), cobrindo design de componentes, reatividade, gerenciamento de estado, roteamento, testes e padrões SSR. Orientações específicas do Nuxt estão incluídas onde diferem do Vue padrão.
Quando Ativar
Ative esta habilidade quando:
- O projeto usa Vue.js (qualquer versão), Nuxt, Vite + Vue ou Pinia.
- O usuário pergunta sobre arquitetura de componentes Vue, composáveis, reatividade ou gerenciamento de estado.
- Revisando Componentes de Arquivo Único Vue (arquivos
.vue). - Configurando Vue Router, stores Pinia, ou configuração Vite/Vitest.
- Discutindo padrões específicos de desempenho, segurança ou SSR do Vue.
1. Estrutura do Projeto
Layout Recomendado (Prioridade a Funcionalidades)
src/
├── api/ # Cliente API e definições de endpoints
├── assets/ # Ativos estáticos (imagens, fontes, ícones)
├── components/ # Componentes compartilhados/reutilizáveis
│ ├── base/ # Primitivos de UI base (Button, Input, Modal)
│ └── features/ # Componentes compartilhados específicos de funcionalidade
├── composables/ # Lógica reutilizável da Composition API
├── layouts/ # Layouts de página (opcional)
├── pages/ # Componentes de página a nível de rota
├── router/ # Configuração do Vue Router
├── stores/ # Stores Pinia
├── types/ # Definições de tipos TypeScript
├── utils/ # Funções utilitárias puras
└── App.vue # Componente raiz
Nomenclatura de Arquivos
| Convenção | Quando Usar |
|---|---|
PascalCase.vue | Todos os componentes (imposto por vue/multi-word-component-names) |
useCamelCase.ts | Composáveis |
camelCase.ts | Utilitários, clientes API, tipos |
Diretórios kebab-case | Segmentos de rota, pastas de funcionalidade |
2. Arquitetura de Componentes
Ordem do Componente de Arquivo Único
<script setup lang="ts">
// 1. Importações (vue → ecossistema → absoluto → relativo)
// 2. Props & Emits & Slots
// 3. Composáveis
// 4. Estado local (ref/reactive)
// 5. Propriedades computadas
// 6. Métodos
// 7. Observadores
// 8. Hooks de ciclo de vida
</script>
<template>
<!-- Conteúdo do template -->
</template>
<style scoped>
/* Estilos escopados */
</style>
Apresentacional vs. Contêiner
- Componentes contêiner: Gerenciam busca de dados, estado e efeitos colaterais. Renderizam componentes apresentacionais.
- Componentes apresentacionais: Recebem props, emitem eventos. Sem chamadas API, sem acesso a stores. Renderização pura.
Melhores Práticas para Props
// Props baseadas em tipo com valores padrão
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
- Sempre forneça
type, erequired/defaultquando apropriado. - Props booleanas:
isXxx,hasXxx,canXxx. - Nunca mude props — emita eventos.
- Para vinculação v-model, use
defineModel()(Vue 3.4+) oumodelValue+update:modelValue.
Eventos
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- Use kebab-case em templates (
@update:model-value). - Use camelCase no script (
emit("update:modelValue", val)).
3. Composáveis (Lógica Reutilizável)
Estrutura
// composables/useDebounce.ts
export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> {
const debounced = ref(toValue(value)) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
Regras
- Deve começar com prefixo
use. - Retorne valores reativos (
ref,computed,reactive), nunca primitivos simples. - Aceite entradas reativas via
MaybeRef/toRef()/toValue(). - Limpe efeitos colaterais em
onUnmountedou noonCleanupdo observador. - Sem efeitos colaterais em escopo de módulo.
vs. Mixins
Composáveis substituem completamente os mixins do Vue 2:
- Mixins: Fluxo de dados opaco, colisões de fonte de verdade, conflitos de nome.
- Composáveis: Importações explícitas, valores de retorno claros, composáveis e tree-shakable.
4. Gerenciamento de Estado
Quando Usar o Quê
| Padrão | Caso de Uso |
|---|---|
ref() / reactive() | Estado local do componente |
| Props + Emits | Comunicação pai-filho |
| Provide / Inject | Tema, configuração, API de plugin |
| Store Pinia | Estado global, compartilhado, complexo |
| Composável de estado do servidor | Dados de API com cache (encapsule fetch/TanStack Query) |
Store Pinia (Setup Store) (Preferida)
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
- Use a sintaxe Setup Store (não Options Store).
- Prefira actions para mutações de nível de negócio e
$patch()para atualizações agrupadas. - Toda action assíncrona: lide com carregamento + sucesso + erro.
5. Vue Router
Definições de Rota
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // lazy
props: true, // pass params as props
meta: { requiresAuth: true },
},
];
Guards de Navegação
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
Parâmetros de Rota Reativos
Quando um componente permanece montado mas os parâmetros da rota mudam:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. Padrões de Template
Sintaxe do Template
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">Carregando...</div>
<div v-else-if="error">Erro: {{ error }}</div>
<div v-else>{{ content }}</div>
<!-- v-show para alternâncias frequentes -->
<div v-show="isOpen">Conteúdo alternado</div>
<!-- v-for com chaves estáveis -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Lista filtrada computada (não v-if + v-for no mesmo elemento) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- Manipulação de eventos -->
<form @submit.prevent="handleSubmit">
<button type="submit">Salvar</button>
</form>
<!-- v-model -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />
7. Desempenho
| Técnica | Quando Usar |
|---|---|
v-memo | Itens de lista que raramente mudam |
v-once | Conteúdo renderizado uma vez e estático para sempre |
shallowRef() | Grandes estruturas de dados substituídas por inteiro |
shallowReactive() | Apenas propriedades de nível superior são reativas |
v-show em vez de v-if | Alternâncias frequentes de visibilidade |
<KeepAlive :max="10"> | Cache de visualizações alternadas |
| Rotas lazy | () => import(...) para rotas não críticas |
Suspense | Carregamento de componentes assíncronos com fallback |
8. Testes
Pilha
- Vitest para testes unitários e de componentes
- Vue Test Utils para montagem e interação
- @pinia/testing para mocks de store
- Playwright para E2E
Padrão de Teste de Componente
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("renderiza e emite", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
9. Padrões Específicos do Nuxt
Auto-Importações
O Nuxt importa automaticamente ref, computed, watch, useFetch, useAsyncData, etc. Use-os diretamente sem importar. Para projetos não-Nuxt, sempre importe explicitamente.
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // chave única para cache
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // deduplica requisições
});
Rotas do Servidor
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... buscar e retornar
});
Configuração em Tempo de Execução
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// apenas no servidor
apiSecret: "",
// público (exposto ao cliente)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Novas APIs do Vue 3.5+
Desestruturação Reativa de Props
O Vue 3.5 estabilizou a desestruturação reativa de props — variáveis desestruturadas de defineProps() são automaticamente reativas:
// Vue 3.5+: props desestruturadas são reativas (não precisa de toRefs)
const { count = 0, msg = "hello" } = defineProps<{
count?: number;
msg?: string;
}>();
// Limitação: não é possível observar uma prop desestruturada diretamente
watch(() => count, (newVal) => { ... }); // PASS getter necessário
useTemplateRef()
Substitua refs simples com correspondência de nome por useTemplateRef() para referências de template:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" corresponde ao atributo ref="input" no template, não ao nome da variável
Suporta IDs de ref dinâmicos: useTemplateRef(dynamicRefId).
onWatcherCleanup()
API de limpeza de observador importável globalmente (Vue 3.5+). Deve ser chamada sincronamente dentro do callback do observador:
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... buscar com signal
});
useId()
Geração de ID única estável para SSR para elementos de formulário e acessibilidade:
import { useId } from "vue";
const id = useId();
defer Teleport
<Teleport defer> permite teletransportar para alvos renderizados no mesmo ciclo:
<Teleport defer to="#container">Conteúdo</Teleport>
<div id="container"></div>
Hidratação Lazy (SSR)
defineAsyncComponent() agora suporta estratégia de hydrate:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
Anti-Padrões
| Anti-Padrão | Por Que Está Errado | A Correção |
|---|---|---|
Destructuring defineProps() (Vue < 3.5) | Captura snapshot, perde reatividade | Acesse via props.xxx ou use toRefs() |
watch() em prop desestruturada (Vue 3.5+) | Erro em tempo de compilação — props desestruturadas não podem ser observadas diretamente | Use wrapper getter: watch(() => count, ...) |
v-if + v-for no mesmo elemento | Ordem de execução ambígua | Use array filtrado computado |
v-for com chave = índice | Estado quebrado ao reordenar | Use IDs estáveis de banco de dados |
| Mutação de props | Viola fluxo de dados unidirecional | Emita eventos ou use v-model |
v-html com conteúdo do usuário | Vulnerabilidade XSS | Sanitize com DOMPurify |
| Mixins no Vue 3 | Opaco, propenso a colisões | Substitua por composáveis |
| Efeitos colaterais em escopo de módulo no composável | Compartilhados entre instâncias | Escopo em onMounted + onUnmounted |
reactive() para estado substituível | Substituição quebra a reatividade | Use ref() em vez |
| Observador sem limpeza | Vazamentos de memória, condições de corrida | Use onCleanup ou onWatcherCleanup() (Vue 3.5+) |
| Options API em novo código Vue 3 | Ecossistema migra para Composition API | Use <script setup> |
| Ref simples para referências de template | Sem suporte a ref dinâmico, correspondência de nome frágil | Use useTemplateRef() (Vue 3.5+) |
Habilidades Relacionadas
accessibility— ARIA, HTML semântico, gerenciamento de focofrontend-patterns— Arquitetura frontend entre frameworkstypescript— Melhores práticas TypeScript aplicadas a projetos Vuecoding-standards— Padrões gerais de qualidade de código