Você já sabe montar um componente Vue. A pergunta é se ele vai continuar fazendo sentido daqui a três meses, quando outra pessoa (ou você mesmo, sem memória) abrir o arquivo. Boas práticas no Vue não são regra de estilo — são o que separa um componente que você entende de relance de um que precisa ser depurado toda vez.

script setup não é açúcar, é o padrão
Se você ainda escreve setup() retornando um objeto, pare. <script setup> é o default moderno: menos boilerplate, melhor inferência de tipo, e tudo declarado no topo já está exposto ao template. Composition API com script setup deixa a lógica agrupada por assunto, não espalhada entre data, methods e computed como no Options API. Você lê de cima a baixo e entende o que o componente faz.
Componente grande é bug esperando acontecer
Um SFC de 400 linhas com seis responsabilidades é impossível de testar e horrível de reusar. Quebre. Se um trecho do template tem estado próprio e nome próprio, é um componente. A regra prática: se você precisa rolar a tela pra ver o <script> inteiro, provavelmente já passou do ponto. Componente pequeno também torna a reatividade previsível — menos estado num lugar só, menos efeito colateral surpresa.
Props tipadas e emits explícitos
Props sem tipo são contrato quebrado esperando acontecer. Use a assinatura genérica:
const props = defineProps<{ userId: number; active?: boolean }>()
const emit = defineEmits<{ (e: 'update', value: string): void }>()
Agora o editor te avisa quando alguém passa userId como string, e o emit é rastreável — nada de eventos fantasma que ninguém sabe de onde vêm. Props são read-only: mutar prop dentro do filho é o clássico bug que some e volta. Emitiu, o pai decide.
computed no lugar de watcher (quase sempre)
Esse é o erro mais comum de quem vem de outros frameworks. Precisa de um valor derivado de outro? computed. Ele é cacheado, declarativo, e reavalia sozinho quando a dependência muda:
const fullName = computed(() => `${first.value} ${last.value}`)
Watcher é para efeito colateral — disparar um fetch, sincronizar com localStorage, logar. Se o seu watch termina atribuindo a outra variável reativa, quase sempre era um computed disfarçado. Watcher demais é sinal de estado que devia ser derivado.
Pinia para estado global, e só
Estado que dois componentes distantes compartilham vai pra store. Pinia é o padrão hoje: tipado, sem mutations, com state/getters/actions limpos. Mas cuidado com o exagero oposto — nem tudo é global. Estado que só um componente e seus filhos usam fica local ou desce via props. Store lotada de coisa que era local vira um balde de estado que ninguém entende. Global é para o que é global de verdade: usuário logado, tema, carrinho.
key estável em v-for não é detalhe
:key diz ao Vue qual item é qual entre renders. Usar o índice do array como key é pedir bug: reordenou, filtrou ou removeu um item e o Vue reaproveita o nó errado — input com valor trocado, componente com estado grudado no lugar errado. Use um id estável e único do próprio dado:
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
Índice só serve pra lista estática que nunca muda de ordem. Na dúvida, id.
Nenhuma dessas coisas é difícil. São hábitos. O componente que segue todos eles não é mais inteligente — é só mais chato de errar, e é exatamente isso que você quer num código que vai durar.

