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, teste e padrões SSR. Orientações específicas do Nuxt são incluídas onde diferem do Vue puro.
Quando Ativar
Ative este skill quando:
- O projeto usa Vue.js (qualquer versão), Nuxt, Vite + Vue ou Pinia.
- O usuário pergunta sobre arquitetura de componentes Vue, composables, reatividade ou gerenciamento de estado.
- Revisando Componentes de Arquivo Único Vue (arquivos
.vue). - Configurando Vue Router, stores Pinia ou configuração Vite/Vitest.
- Discutindo desempenho, segurança ou padrões SSR específicos do Vue.
1. Estrutura do Projeto
Layout Recomendado (Feature-First)
src/
├── api/ # Cliente de 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 funcionalidades
├── composables/ # Lógica reutilizável da Composition API
├── layouts/ # Layouts de página (opcional)
├── pages/ # Componentes de página no nível de rota
├── router/ # Configuração do Vue Router
├── stores/ # Stores Pinia
├── types/ # Definições de tipo 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 | Composables |
camelCase.ts | Utilitários, clientes de API, tipos |
Diretórios kebab-case | Segmentos de rota, pastas de funcionalidades |
2. Arquitetura de Componentes
Ordem do Componente de Arquivo Único
<script setup lang="ts">
// 1. Imports (vue → ecossistema → absoluto → relativo)
// 2. Props & Emits & Slots
// 3. Composables
// 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 Container
- Componentes container: Possuem busca de dados, estado e efeitos colaterais. Renderizam componentes apresentacionais.
- Componentes apresentacionais: Recebem props, emitem eventos. Sem chamadas de API, sem acesso a store. Renderização pura.
Melhores Práticas de Props
// Props baseadas em tipos 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 mutar props — emita eventos.
- Para vinculação
v-model, usedefineModel()(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. Composables (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 no escopo do módulo.
vs Mixins
Composables substituem completamente os mixins do Vue 2:
- Mixins: Fluxo de dados opaco, colisões de fonte da verdade, conflitos de nome.
- Composables: Imports explícitos, valores de retorno claros, combiná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 |
| Composable de estado servidor | Dados de API com cache (envolva fetch/TanStack Query) |
Store Pinia Setup (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 de Setup Store (não Options Store).
- Prefira ações para mutações de nível de negócio e
$patch()para atualizações agrupadas. - Toda ação assíncrona: trate 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, // Passa params como 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 de rota mudam:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. Padrões de Template
Sintaxe de 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 completo |
shallowReactive() | Apenas propriedades de nível superior são reativas |
v-show sobre 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 componente assíncrono com fallback |
8. Teste
Stack
- Vitest para testes de unidade e componente
- Vue Test Utils para montagem e interação
- @pinia/testing para mock 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-Imports
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 de 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 de Runtime
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// apenas servidor
apiSecret: "",
// público (exposto ao cliente)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Novas APIs Vue 3.5+
Desestruturação Reativa de Props
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 (sem necessidade de toRefs)
const { count = 0, msg = "olá" } = defineProps<{
count?: number;
msg?: string;
}>();
// Limitação: não é possível observar prop desestruturada diretamente
watch(() => count, (newVal) => { ... }); // PRECISA de getter
useTemplateRef()
Substitua refs simples correspondentes por nome com 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 sinal
});
useId()
Geração de ID único 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 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 |
|---|---|---|
Desestruturar defineProps() (Vue < 3.5) | Captura snapshot, perde reatividade | Acesse via props.xxx ou use toRefs() |
watch() em prop desestruturada (Vue 3.5+) | Erro 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 key = índice | Estado quebrado ao reordenar | Use IDs de banco de dados estáveis |
| Mutar 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 composables |
| Efeitos colaterais no escopo do módulo em composable | Compartilhado entre instâncias | Escopo em onMounted + onUnmounted |
reactive() para estado substituível | Substituição quebra reatividade | Use ref() |
| 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 migrando 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+) |
Skills Relacionados
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