DevTools

Cheatsheet Vue.js

Framework frontend progressivo para interfaces reativas

Voltar às linguagens
Vue.js
93 cards encontrados
Categorias:
Versões:

Setup e Estrutura


10 cards
Criar projeto (Vite)
# Criar com create-vue (oficial):
npm create vue@latest meu-projeto
cd meu-projeto
npm install
npm run dev

# Opções durante setup:
# - TypeScript? Sim/Não
# - JSX? Sim/Não
# - Vue Router? Sim/Não
# - Pinia? Sim/Não
# - Vitest? Sim/Não
# - ESLint + Prettier? Sim/Não

# Alternativa manual:
npm create vite@latest app -- --template vue

create-vue é o scaffold oficial (baseado em Vite). Configura TypeScript, Router, Pinia e testes conforme escolha. npm run dev inicia servidor com HMR (Hot Module Replacement). Estrutura gerada: src/, public/, vite.config.js. Vite é muito mais rápido que Webpack em desenvolvimento.

Estrutura de pastas
src/
├── assets/          # imagens, fonts
├── components/      # componentes reutilizáveis
│   ├── AppHeader.vue
│   └── AppFooter.vue
├── composables/     # lógica partilhada (use*)
│   └── useContador.js
├── views/           # páginas (rotas)
│   ├── HomeView.vue
│   └── AboutView.vue
├── router/          # configuração de rotas
│   └── index.js
├── stores/          # Pinia stores
│   └── contador.js
├── App.vue          # componente raiz
└── main.js          # entry point

Estrutura padrão de projecto Vue. components/ para UI reutilizável. views/ (ou pages/) para componentes de rota. composables/ para lógica partilhada (prefixo use). stores/ para estado global (Pinia). router/ para navegação. Convenção: PascalCase para ficheiros .vue.

Directivas built-in (resumo)
{{ texto }}           <!-- interpolação -->
v-bind:src="url"      <!-- :src atalho -->
v-on:click="fn"       <!-- @click atalho -->
v-model="valor"       <!-- two-way binding -->
v-if="cond"           <!-- condicional -->
v-else-if="outra"
v-else
v-show="visivel"      <!-- display toggle -->
v-for="item in lista" <!-- iteração -->
v-html="markup"       <!-- HTML raw -->
v-text="texto"        <!-- textContent -->
v-slot:nome           <!-- #nome atalho -->
v-pre                 <!-- não compilar -->
v-once                <!-- renderizar 1x -->
v-memo="[dep]"        <!-- memoizar -->

Vue tem directivas built-in prefixadas com v-. Atalhos: : para v-bind, @ para v-on, # para v-slot. v-if remove/cria DOM; v-show usa display:none. v-model para two-way binding. v-once e v-memo para optimização.

Single File Component (SFC)
<!-- MeuComponente.vue -->
<template>
  <div class="caixa">
    <h1>{{ titulo }}</h1>
  </div>
</template>

<script setup>
import { ref } from "vue";
const titulo = ref("Olá Vue");
</script>

<style scoped>
.caixa {
  padding: 1rem;
  border: 1px solid #ccc;
}
</style>

SFC combina template, script e style num ficheiro .vue. <script setup> é a sintaxe moderna (Composition API). <style scoped> limita CSS ao componente (não afecta outros). Cada SFC é um componente reutilizável. O compilador Vue transforma SFC em JavaScript render functions.

Via CDN (sem build)
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <p>{{ mensagem }}</p>
    <button @click="contador++">
      Cliques: {{ contador }}
    </button>
  </div>

  <script>
    const { createApp, ref } = Vue;
    createApp({
      setup() {
        const mensagem = ref("Olá!");
        const contador = ref(0);
        return { mensagem, contador };
      }
    }).mount("#app");
  </script>
</body>
</html>

Uso rápido sem build: incluir Vue via CDN e usar Vue.createApp(). Sem SFC, sem importações — tudo no HTML. setup() retorna objecto com dados para o template. Ideal para protótipos e aprendizagem. Para produção, usar Vite com build (tree-shaking, optimizações). vue.global.js expõe tudo no objecto Vue.

Directivas customizadas
<script setup>
// Directiva local (prefixo v- automático):
const vFocus = {
  mounted(el) {
    el.focus();
  }
};

// Com mais hooks:
const vCor = {
  mounted(el, binding) {
    el.style.color = binding.value;
  },
  updated(el, binding) {
    el.style.color = binding.value;
  }
};
</script>

<template>
  <input v-focus />
  <p v-cor="'red'">Texto vermelho</p>
  <p v-cor="corDinamica">Cor dinâmica</p>
</template>

Directivas customizadas para manipulação directa do DOM. Em script setup, variável vNome regista como v-nome. Hooks: mounted, updated, unmounted. binding.value é o valor passado. Usar quando ref + lifecycle não bastam. Registar globalmente com app.directive().

createApp e mount
// main.js — ponto de entrada:
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";

const app = createApp(App);

app.use(router);
app.use(createPinia());

// Plugin/diretiva global:
app.directive("focus", {
  mounted(el) { el.focus(); }
});

app.mount("#app");

createApp() cria a instância raiz. app.use() regista plugins (Router, Pinia). app.directive() regista directivas globais. app.mount("#app") monta no elemento HTML. Tudo registado antes de mount(). O App.vue é o componente raiz que contém <router-view>.

vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(
        new URL("./src", import.meta.url)
      ),
    },
  },
  server: {
    port: 3000,
    proxy: {
      "/api": "http://localhost:8000",
    },
  },
});

vite.config.js configura o build. @vitejs/plugin-vue compila SFC. alias "@" mapeia para src/ (imports mais curtos). server.port define porta do dev server. proxy redirecciona requests API (evita CORS em dev). Vite detecta mudanças e recarrega automaticamente (HMR).

script setup vs Options API
<!-- Composition API (recomendado): -->
<script setup>
import { ref, computed } from "vue";
const n = ref(0);
const dobro = computed(() => n.value * 2);
function inc() { n.value++; }
</script>

<!-- Options API (clássico): -->
<script>
export default {
  data() { return { n: 0 }; },
  computed: {
    dobro() { return this.n * 2; }
  },
  methods: {
    inc() { this.n++; }
  }
};
</script>

<script setup> é a sintaxe moderna: menos boilerplate, melhor TypeScript, variáveis usadas directamente no template. Options API organiza por tipo (data, methods, computed). Composition API permite agrupar lógica relacionada. Ambos funcionam em Vue 3. script setup é o padrão recomendado pela equipa Vue.

TypeScript em Vue
<script setup lang="ts">
import { ref, computed } from "vue";

// Tipagem automática:
const nome = ref<string>("Ana");
const idade = ref(30); // inferido: number

// Props tipadas:
const props = defineProps<{
  titulo: string;
  contador?: number;
}>();

// Emits tipados:
const emit = defineEmits<{
  salvar: [dados: string];
  cancelar: [];
}>();

// Composable tipado:
const items = ref<string[]>([]);
</script>

lang="ts" activa TypeScript no SFC. ref<T>() para tipagem explícita (ou inferência automática). defineProps<T>() com tipos TypeScript (sem runtime). defineEmits<T>() com tuple labels. Vue 3 + TypeScript tem suporte de primeira classe. IDE: Volar (extensão oficial) para type-checking em templates.

Template e Diretivas


11 cards
Interpolação (Mustache)
<template>
  <!-- Texto: -->
  <p>{{ mensagem }}</p>
  <p>{{ 1 + 1 }}</p>
  <p>{{ ativo ? "Sim" : "Não" }}</p>
  <p>{{ nome.toUpperCase() }}</p>

  <!-- NÃO funciona em atributos: -->
  <!-- <img src="{{ url }}"> ← ERRO -->
  <!-- Usar v-bind: -->
  <img :src="url" />

  <!-- Renderizar 1x (sem reactividade): -->
  <p v-once>{{ estatico }}</p>
</template>

{{ }} (mustache) renderiza texto reactivo. Aceita expressões JavaScript simples (ternário, métodos). Não funciona em atributos HTML — usar v-bind (:attr). Actualiza automaticamente quando dados mudam. v-once renderiza apenas uma vez (optimização). Não usar para HTML — usar v-html.

v-model (two-way binding)
<template>
  <!-- Texto: -->
  <input v-model="nome" placeholder="Nome" />

  <!-- Textarea: -->
  <textarea v-model="descricao"></textarea>

  <!-- Checkbox: -->
  <input type="checkbox" v-model="aceite" id="termos" />

  <!-- Radio: -->
  <input type="radio" v-model="tipo" value="admin" />
  <input type="radio" v-model="tipo" value="user" />

  <!-- Select: -->
  <select v-model="pais">
    <option value="">Escolher...</option>
    <option value="pt">Portugal</option>
    <option value="br">Brasil</option>
  </select>

  <!-- Modificadores: -->
  <input v-model.trim="email" />
  <input v-model.number="idade" type="number" />
  <input v-model.lazy="busca" />
</template>

v-model cria ligação bidireccional (input → dado → input). Funciona com input, textarea, select, checkbox, radio. Modificadores: .trim remove espaços, .number converte para número, .lazy actualiza no change em vez de input. Em componentes, equivale a :modelValue + @update:modelValue.

Slots (conteúdo projectado)
<!-- Cartao.vue (componente): -->
<template>
  <div class="cartao">
    <slot>Conteúdo padrão</slot>
  </div>
</template>

<!-- Uso: -->
<Cartao>
  <p>Meu conteúdo custom</p>
</Cartao>

<!-- Sem conteúdo → mostra "Conteúdo padrão" -->
<Cartao />

<!-- Named slots: -->
<!-- Layout.vue: -->
<template>
  <header><slot name="topo"></slot></header>
  <main><slot></slot></main>
  <footer><slot name="rodape"></slot></footer>
</template>

<!-- Uso com # (atalho v-slot): -->
<Layout>
  <template #topo><h1>Título</h1></template>
  <p>Conteúdo principal</p>
  <template #rodape><small>© 2024</small></template>
</Layout>

<slot> define pontos de inserção de conteúdo. Sem conteúdo → mostra fallback. Named slots (name="x") para múltiplas áreas. Atalho #nome = v-slot:nome. Slot default não precisa de template wrapper. Slots permitem composição flexível sem props. Base de design systems e layouts.

v-bind (binding de atributos)
<!-- Sintaxe completa e atalho: -->
<img v-bind:src="url" />
<img :src="url" />

<!-- Múltiplos atributos: -->
<a :href="link" :title="descricao" :target="externo ? '_blank' : '_self'">
  Link
</a>

<!-- Objecto de atributos (spread): -->
<div v-bind="{ id: 'caixa', class: 'ativo', 'data-x': 1 }"></div>

<!-- Class dinâmica: -->
<div :class="{ ativo: estaAtivo, erro: temErro }"></div>
<div :class="['base', estaAtivo ? 'ativo' : '']"></div>

<!-- Style dinâmico: -->
<div :style="{ color: cor, fontSize: tamanho + 'px' }"></div>

v-bind (atalho :) liga atributos a expressões reactivas. :class aceita objecto (toggle por boolean) ou array. :style aceita objecto CSS (camelCase). v-bind="obj" aplica múltiplos atributos de uma vez. Essencial para qualquer atributo dinâmico. Sem :, o valor é string literal.

v-html e v-text
<template>
  <!-- v-text (seguro, escapa HTML): -->
  <p v-text="mensagem"></p>
  <!-- Equivale a: <p>{{ mensagem }}</p> -->

  <!-- v-html (interpreta HTML): -->
  <div v-html="conteudoHtml"></div>

  <!-- CUIDADO: XSS se dados do utilizador! -->
  <!-- Nunca: v-html="inputUtilizador" -->
  <!-- Sanitizar antes: -->
  <div v-html="sanitizado"></div>
</template>

<script setup>
import { ref } from "vue";
import DOMPurify from "dompurify";

const conteudoHtml = ref("<strong>Negrito</strong> e <em>itálico</em>");
const sanitizado = ref(DOMPurify.sanitize(conteudoHtml.value));
</script>

v-html renderiza HTML raw (interpreta tags). v-text define textContent (escapa tudo, seguro). {{ }} é equivalente a v-text. v-html é vulnerável a XSS — nunca usar com input do utilizador sem sanitizar. DOMPurify é a biblioteca padrão para sanitização.

Scoped slots
<!-- Lista.vue (componente): -->
<template>
  <ul>
    <li v-for="item in itens" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.nome }}
      </slot>
    </li>
  </ul>
</template>

<!-- Uso (pai decide como renderizar): -->
<Lista :itens="produtos">
  <template #default="{ item }">
    <strong>{{ item.nome }}</strong>
    <span>{{ item.preco }}€</span>
  </template>
</Lista>

<!-- Outro uso (layout diferente): -->
<Lista :itens="users">
  <template #default="{ item }">
    <img :src="item.avatar" />
    {{ item.email }}
  </template>
</Lista>

Scoped slots passam dados do filho para o pai via atributos no <slot>. O pai recebe com #default="{ item }" (destructuring). O componente controla a lógica; o pai controla a renderização. Padrão "renderless component" — componente sem markup próprio. Essencial para tabelas, listas e autocomplete customizáveis.

v-if / v-else-if / v-else
<template>
  <p v-if="estado === 'loading'">A carregar...</p>
  <p v-else-if="estado === 'erro'">Falhou!</p>
  <p v-else>Conteúdo: {{ dados }}</p>

  <!-- Com template (não renderiza div extra): -->
  <template v-if="logado">
    <header>Bem-vindo</header>
    <nav>Menu</nav>
  </template>

  <!-- v-show (alternativa): -->
  <p v-show="visivel">Toggle com display</p>
</template>

<script setup>
import { ref } from "vue";
const estado = ref("loading");
const logado = ref(true);
const visivel = ref(false);
</script>

v-if cria/destrói elementos no DOM (renderização condicional real). v-show apenas alterna display:none (elemento sempre no DOM). <template v-if> agrupa sem renderizar wrapper. Usar v-if para condições raras; v-show para toggles frequentes (menos re-render). Aceita qualquer expressão truthy/falsy.

Class e style dinâmicos
<template>
  <!-- Objecto (toggle por boolean): -->
  <div :class="{ ativo: estaAtivo, 'texto-grande': grande }">
  </div>

  <!-- Array: -->
  <div :class="[classeBase, estaAtivo ? 'ativo' : '']">
  </div>

  <!-- Combinar com class estática: -->
  <div class="card" :class="{ 'card-ativo': selecionado }">
  </div>

  <!-- Style object: -->
  <div :style="{ color: corTexto, fontSize: tam + 'px' }">
  </div>

  <!-- Múltiplos styles: -->
  <div :style="[estiloBase, estiloOverride]"></div>
</template>

<script setup>
import { ref } from "vue";
const estaAtivo = ref(true);
const corTexto = ref("#333");
const tam = ref(16);
</script>

:class aceita objecto ({classe: boolean}), array ou combinação com class estática. Nomes com hífen precisam de aspas. :style usa camelCase para propriedades CSS. Ambos são reactivos — actualizam quando dados mudam. Preferir :class a :style (sepção de responsabilidades, cache CSS).

Refs no template
<template>
  <input ref="campoNome" />
  <div ref="caixa">Conteúdo</div>
  <button @click="focar">Focar input</button>

  <!-- Ref em v-for: -->
  <li v-for="item in itens" :key="item.id" ref="itemRefs">
    {{ item.nome }}
  </li>
</template>

<script setup>
import { ref, onMounted } from "vue";

const campoNome = ref(null);
const caixa = ref(null);

function focar() {
  campoNome.value.focus();
}

onMounted(() => {
  console.log(caixa.value.offsetWidth);
});
</script>

ref="nome" no template + const nome = ref(null) no script cria referência directa ao elemento DOM. Aceder com nome.value (elemento nativo). Em v-for, o ref é um array de elementos. Usar para focus(), medições e APIs nativas. Preferir reactividade a manipulação directa sempre que possível.

v-for (listas)
<template>
  <!-- Array: -->
  <li v-for="(item, index) in itens" :key="item.id">
    {{ index + 1 }}. {{ item.nome }}
  </li>

  <!-- Objecto: -->
  <div v-for="(valor, chave, i) in utilizador" :key="chave">
    {{ chave }}: {{ valor }}
  </div>

  <!-- Range: -->
  <span v-for="n in 10" :key="n">{{ n }}</span>

  <!-- Com filtro (computed): -->
  <li v-for="item in itensActivos" :key="item.id">
    {{ item.nome }}
  </li>
</template>

<script setup>
import { computed } from "vue";
const itensActivos = computed(
  () => itens.value.filter(i => i.ativo)
);
</script>

v-for itera arrays, objectos e ranges. :key é obrigatório (identificador único para diff eficiente). Segundo parâmetro dá o índice. Para objectos: (valor, chave, índice). Não usar v-if no mesmo elemento que v-for — filtrar com computed. key nunca deve ser o índice (se lista muda).

Renderização condicional avançada
<template>
  <!-- Componente dinâmico: -->
  <component :is="componenteActual" />

  <!-- Com KeepAlive (cache): -->
  <KeepAlive>
    <component :is="aba" :key="aba" />
  </KeepAlive>

  <!-- Ternário no template: -->
  <p>{{ logado ? "Bem-vindo" : "Faça login" }}</p>

  <!-- Operador && (short-circuit): -->
  <p v-if="erros.length && mostrarErros">
    {{ erros[0] }}
  </p>
</template>

<script setup>
import { shallowRef } from "vue";
import FormLogin from "./FormLogin.vue";
import FormRegisto from "./FormRegisto.vue";

const componenteActual = shallowRef(FormLogin);
</script>

<component :is> renderiza componentes dinamicamente. Aceita nome (string) ou referência. shallowRef para componentes (evita reactividade profunda). KeepAlive preserva estado ao trocar. :key força recriação quando necessário. Ternários para condições simples inline. v-if para blocos complexos.

Eventos e Métodos


10 cards
v-on e @ (handlers)
<template>
  <!-- Sintaxe completa e atalho: -->
  <button v-on:click="salvar">Salvar</button>
  <button @click="salvar">Salvar</button>

  <!-- Expressão inline: -->
  <button @click="contador++">+1</button>

  <!-- Com argumento: -->
  <button @click="remover(item.id)">Remover</button>

  <!-- Múltiplos handlers: -->
  <button @click="validar(), enviar()">Enviar</button>

  <!-- Aceder ao evento nativo: -->
  <button @click="handler($event)">Click</button>

  <!-- Expressão + evento: -->
  <input @input="nome = $event.target.value" />
</template>

v-on (atalho @) liga eventos DOM a handlers. Aceita nome de função, expressão inline ou ambas. $event dá acesso ao evento nativo. Múltiplos handlers separados por vírgula. Funciona com qualquer evento DOM: click, input, submit, keydown, etc. Em componentes, ouve emits custom.

v-model em componentes
<!-- Pai: -->
<MeuInput v-model="nome" />
<!-- Equivale a: -->
<MeuInput
  :modelValue="nome"
  @update:modelValue="nome = $event"
/>

<!-- Filho (MeuInput.vue): -->
<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>
<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<!-- Múltiplos v-model (Vue 3): -->
<UserForm v-model:nome="n" v-model:email="e" />

v-model em componente é açúcar para :modelValue + @update:modelValue. O filho recebe via props e emite update:modelValue. Vue 3 permite múltiplos v-model nomeados (v-model:nome). Substitui o .sync do Vue 2. Padrão para inputs, selects e toggles customizados.

Formulários e eventos
<template>
  <form @submit.prevent="enviar">
    <input v-model="form.nome" @blur="validarCampo('nome')" />
    <span v-if="erros.nome" class="erro">{{ erros.nome }}</span>

    <input v-model="form.email" type="email" />
    <select v-model="form.pais">
      <option v-for="p in paises" :key="p" :value="p">{{ p }}</option>
    </select>

    <button :disabled="!formValido">Enviar</button>
  </form>
</template>

<script setup>
import { reactive, computed } from "vue";
const form = reactive({ nome: "", email: "", pais: "" });
const erros = reactive({ nome: "", email: "" });

const formValido = computed(() =>
  form.nome.length >= 3 && form.email.includes("@")
);

function enviar() { /* AJAX */ }
</script>

Padrão de formulário Vue: v-model para binding, @submit.prevent para submit sem reload. reactive() para objecto do formulário. computed para validação derivada. @blur para validação por campo. :disabled reactivo no botão. Erros em objecto separado. Limpar erros no @input do campo.

Modificadores de evento
<template>
  <!-- .stop: stopPropagation -->
  <div @click="pai">
    <button @click.stop="filho">Não propaga</button>
  </div>

  <!-- .prevent: preventDefault -->
  <form @submit.prevent="enviar">...</form>

  <!-- .once: só dispara 1x -->
  <button @click.once="iniciar">Iniciar</button>

  <!-- .self: só se target é o próprio -->
  <div @click.self="fechar">Overlay</div>

  <!-- Encadear: -->
  <a @click.stop.prevent="handler">Link</a>

  <!-- .capture: fase de captura -->
  <div @click.capture="antes">...</div>
</template>

Modificadores são suffixos após @evento.. .stop = stopPropagation(). .prevent = preventDefault(). .once remove handler após 1ª execução. .self ignora cliques em filhos. .capture ouve na fase de captura (antes dos filhos). Encadeáveis. Eliminam necessidade de chamar métodos do evento manualmente.

Eventos nativos em componentes
<!-- Vue 3: eventos nativos caem no root (fallthrough) -->
<MeuBotao @click="handler" class="extra" />
<!-- click e class vão para o elemento raiz do componente -->

<!-- Desactivar fallthrough: -->
<script setup>
defineOptions({ inheritAttrs: false });
</script>

<!-- Aplicar manualmente: -->
<template>
  <div class="wrapper">
    <button v-bind="$attrs">Texto</button>
  </div>
</template>

<!-- $attrs contém: class, style, eventos, atributos -->
<script setup>
// Aceder:
import { useAttrs } from "vue";
const attrs = useAttrs();
</script>

Em Vue 3, atributos/eventos não declarados caem no elemento raiz do componente (fallthrough). inheritAttrs: false desactiva. $attrs (ou useAttrs()) contém todos os atributos/eventos não declarados como props. v-bind="$attrs" aplica noutro elemento. Útil para wrapper components (inputs, botões).

Debounce em eventos
<script setup>
import { ref } from "vue";

const busca = ref("");
const resultados = ref([]);

// Debounce manual:
let timer;
function aoEscrever(e) {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    resultados.value = await pesquisar(busca.value);
  }, 300);
}

// Com lodash:
// import { debounce } from "lodash-es";
// const aoEscrever = debounce(async () => {
//   resultados.value = await pesquisar(busca.value);
// }, 300);

// Com watch (alternativa):
import { watchDebounced } from "@vueuse/core";
watchDebounced(busca, async (v) => {
  resultados.value = await pesquisar(v);
}, { debounce: 300 });
</script>

<template>
  <input v-model="busca" @input="aoEscrever" />
</template>

Debounce evita chamadas excessivas em inputs de pesquisa. clearTimeout + setTimeout manual. lodash debounce para versão robusta. VueUse tem watchDebounced (combina watch + debounce). 300ms é um bom default. Essencial para autocomplete, pesquisa em tempo real e validação assíncrona.

Modificadores de tecla
<template>
  <!-- Teclas específicas: -->
  <input @keyup.enter="enviar" />
  <input @keydown.esc="fechar" />
  <input @keyup.tab="proximo" />
  <input @keydown.delete="limpar" />

  <!-- Combinações: -->
  <input @keydown.ctrl.s="guardar" />
  <input @keydown.ctrl.shift.p="paleta" />
  <input @keyup.alt.enter="novaLinha" />

  <!-- Global (com directiva ou composable): -->
  <!-- useEventListener(document, 'keydown', fn) -->
</template>

<script setup>
function guardar(e) {
  e.preventDefault();
  salvarDados();
}
</script>

Modificadores de tecla: .enter, .esc, .tab, .delete, .space, .up, .down, .left, .right. Combinações: .ctrl, .shift, .alt, .meta (Cmd). Para teclas custom: @keyup.f2. keydown para atalhos (com preventDefault); keyup para input. Para listeners globais, usar composable.

Event bus (padrão legado)
// Vue 3 removeu $on/$off/$emit de instâncias
// Alternativa: mitt (biblioteca externa)

// eventBus.js:
import mitt from "mitt";
export const bus = mitt();

// Componente A (emitir):
import { bus } from "./eventBus";
bus.emit("notificacao", { msg: "Olá" });

// Componente B (ouvir):
import { bus } from "./eventBus";
import { onMounted, onUnmounted } from "vue";

const handler = (dados) => console.log(dados);
onMounted(() => bus.on("notificacao", handler));
onUnmounted(() => bus.off("notificacao", handler));

// PREFERIR: provide/inject ou Pinia

Vue 3 removeu o event bus nativo ($on/$off). Alternativa: biblioteca mitt (1kB). Sempre limpar listeners no onUnmounted (memory leak). Para comunicação entre componentes, preferir: props/emits (pai-filho), provide/inject (ancestral-descendente), Pinia (global). Event bus só para casos muito específicos.

defineEmits (comunicação filho→pai)
<!-- Filho.vue: -->
<script setup>
const emit = defineEmits(["salvar", "cancelar"]);

function aoClicar() {
  emit("salvar", { nome: "Ana", idade: 30 });
}
</script>
<template>
  <button @click="aoClicar">Salvar</button>
  <button @click="emit('cancelar')">Cancelar</button>
</template>

<!-- Pai.vue: -->
<script setup>
function aoSalvar(dados) {
  console.log("Recebido:", dados);
}
</script>
<template>
  <Filho @salvar="aoSalvar" @cancelar="fechar" />
</template>

defineEmits() declara eventos que o componente pode emitir. emit("nome", payload) envia dados ao pai. O pai ouve com @nome="handler" (igual a eventos DOM). Fluxo unidireccional: dados descem via props, eventos sobem via emits. Nomes em camelCase no script, kebab-case no template.

useEventListener (composable)
// composables/useEventListener.js:
import { onMounted, onUnmounted } from "vue";

export function useEventListener(target, evento, handler) {
  onMounted(() => target.addEventListener(evento, handler));
  onUnmounted(() => target.removeEventListener(evento, handler));
}

// Uso no componente:
<script setup>
import { useEventListener } from "./useEventListener";

useEventListener(window, "resize", () => {
  largura.value = window.innerWidth;
});

useEventListener(document, "keydown", (e) => {
  if (e.key === "Escape") fechar();
});
</script>

Composable que regista listener no mounted e remove no unmounted automaticamente. Evita memory leaks. Aceita qualquer target (window, document, elemento). Padrão da biblioteca VueUse. Elimina boilerplate repetitivo de add/removeEventListener. Essencial para eventos globais (resize, scroll, keyboard).

Componentes


11 cards
Definir e usar componente
<!-- MeuBotao.vue: -->
<script setup>
</script>
<template>
  <button class="btn">Clique aqui</button>
</template>

<!-- App.vue (usar): -->
<script setup>
import MeuBotao from "./components/MeuBotao.vue";
</script>
<template>
  <MeuBotao />
  <meu-botao />  <!-- kebab-case também funciona -->
</template>

<!-- Registo global (main.js): -->
// app.component("MeuBotao", MeuBotao);
// Não precisa de import em cada uso

Cada ficheiro .vue é um componente. Importar no <script setup> para usar no template. Nomes em PascalCase ou kebab-case. Registo global com app.component() dispensa imports (mas perde tree-shaking). Componentes são reutilizáveis e isolados. Convenção: prefixo App ou Base para genéricos.

Componentes dinâmicos
<template>
  <!-- Tabs com componente dinâmico: -->
  <nav>
    <button v-for="aba in abas" :key="aba"
      :class="{ activo: abaActual === aba }"
      @click="abaActual = aba">
      {{ aba }}
    </button>
  </nav>

  <KeepAlive>
    <component :is="componentes[abaActual]" />
  </KeepAlive>
</template>

<script setup>
import { shallowRef } from "vue";
import TabPerfil from "./TabPerfil.vue";
import TabDefinicoes from "./TabDefinicoes.vue";

const abas = ["Perfil", "Definições"];
const abaActual = shallowRef("Perfil");
const componentes = { Perfil: TabPerfil, Definições: TabDefinicoes };
</script>

<component :is> renderiza componentes dinamicamente. Aceita referência ou string (se registado). KeepAlive preserva estado ao trocar (não destrói). shallowRef para valores de componente (evita reactividade profunda). Padrão tabs/wizard. Sem KeepAlive, estado perde-se a cada troca.

Expose (API pública)
<!-- Filho.vue: -->
<script setup>
import { ref } from "vue";

const inputRef = ref(null);
const interno = ref("secreto");

function focar() {
  inputRef.value.focus();
}

function limpar() {
  inputRef.value.value = "";
}

// Só expor métodos específicos:
defineExpose({ focar, limpar });
</script>
<template>
  <input ref="inputRef" />
</template>

<!-- Pai.vue: -->
<script setup>
import { ref } from "vue";
const filho = ref(null);
</script>
<template>
  <Filho ref="filho" />
  <button @click="filho.focar()">Focar</button>
  <button @click="filho.limpar()">Limpar</button>
</template>

defineExpose() controla o que o pai pode aceder via ref no componente. Em script setup, tudo é privado por defeito. Expor apenas métodos necessários (encapsulamento). Pai acede com ref template + filho.value.metodo(). Alternativa a emits para acções imperativas (focus, scroll, reset).

defineProps (receber dados)
<script setup>
// Simples:
const props = defineProps(["titulo", "valor"]);

// Com tipos e validação:
const props = defineProps({
  titulo: { type: String, required: true },
  contador: { type: Number, default: 0 },
  itens: { type: Array, default: () => [] },
  tamanho: {
    type: String,
    validator: (v) => ["sm", "md", "lg"].includes(v)
  }
});

// TypeScript (generics):
// const props = defineProps<{
//   titulo: string;
//   contador?: number;
// }>();
</script>

<template>
  <h2>{{ titulo }}</h2>
  <p>Contagem: {{ contador }}</p>
</template>

defineProps() declara dados que o componente recebe do pai. type para validação, required para obrigatórios, default para opcionais. validator para regras custom. Props são readonly — nunca mutar directamente. Com TypeScript, usar generics para tipagem completa. Acesso via props.x no script, directo no template.

Async components
<script setup>
import { defineAsyncComponent } from "vue";

// Lazy load (code splitting):
const EditorPesado = defineAsyncComponent(
  () => import("./EditorPesado.vue")
);

// Com loading e erro:
const Painel = defineAsyncComponent({
  loader: () => import("./Painel.vue"),
  loadingComponent: Spinner,
  errorComponent: ErroMsg,
  delay: 200,      // ms antes de mostrar loading
  timeout: 10000,  // ms antes de erro
});
</script>

<template>
  <EditorPesado v-if="mostrarEditor" />
  <Suspense>
    <Painel />
    <template #fallback><p>A carregar...</p></template>
  </Suspense>
</template>

defineAsyncComponent() carrega componentes sob demanda (code splitting). Reduz bundle inicial. loadingComponent mostra spinner durante load. errorComponent para falhas. delay evita flash em loads rápidos. Suspense (experimental) para fallback declarativo. Ideal para componentes pesados (editors, charts, modais raros).

Renderless components
<!-- useMouse.vue (renderless): -->
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const x = ref(0);
const y = ref(0);

function update(e) { x.value = e.pageX; y.value = e.pageY; }
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
</script>

<template>
  <slot :x="x" :y="y" />
</template>

<!-- Uso: -->
<UseMouse v-slot="{ x, y }">
  <p>Rato: {{ x }}, {{ y }}</p>
</UseMouse>

<!-- Outro consumo (layout diferente): -->
<UseMouse v-slot="{ x, y }">
  <div :style="{ left: x+'px', top: y+'px' }" class="cursor" />
</UseMouse>

Renderless component: componente sem markup próprio — só lógica + scoped slot. Separa lógica de renderização. O consumidor decide como mostrar via slot. Alternativa a composables (quando precisa de template). Padrão popular em bibliotecas (VueUse, Headless UI). Hoje, composables são preferidos na maioria dos casos.

Passar props (pai→filho)
<template>
  <!-- Estático (string literal): -->
  <Cartao titulo="Olá Mundo" />

  <!-- Dinâmico (expressão, usa :): -->
  <Cartao :titulo="nomeVariavel" />
  <Cartao :valor="10 + 5" />
  <Cartao :itens="listaFiltrada" />
  <Cartao :activo="true" />

  <!-- Sem : é sempre string: -->
  <Cartao valor="10" />   <!-- string "10" -->
  <Cartao :valor="10" />  <!-- número 10 -->

  <!-- Spread de objecto: -->
  <Cartao v-bind="objProps" />
  <!-- equivale a passar cada propriedade -->
</template>

<script setup>
const objProps = { titulo: "X", valor: 5, activo: true };
</script>

Sem :, o valor é string literal. Com : (v-bind), é expressão JavaScript (números, booleanos, arrays, objectos). v-bind="obj" passa todas as propriedades de um objecto (spread). Props fluem unidireccionalmente (pai→filho). Filho nunca altera props — emite evento para pedir mudança.

provide / inject
<!-- Ancestor (qualquer nível acima): -->
<script setup>
import { provide, ref, readonly } from "vue";

const tema = ref("escuro");
provide("tema", readonly(tema));
provide("mudarTema", (novo) => { tema.value = novo; });
</script>

<!-- Descendant (qualquer nível abaixo): -->
<script setup>
import { inject } from "vue";

const tema = inject("tema");
const mudarTema = inject("mudarTema");
// Com default:
const lang = inject("idioma", "pt");
</script>

<template>
  <p>Tema: {{ tema }}</p>
  <button @click="mudarTema('claro')">Mudar</button>
</template>

provide/inject passa dados entre qualquer nível da árvore (sem prop drilling). Ancestor provide("chave", valor); descendant inject("chave"). Expor readonly() + método de mutação para controle. Segundo parâmetro de inject = default. Ideal para temas, locale, auth. Para estado complexo, preferir Pinia.

Padrões de composição
<!-- Wrapper component (proxy): -->
<!-- MeuInput.vue: -->
<script setup>
defineProps(["modelValue", "label", "erro"]);
defineEmits(["update:modelValue"]);
</script>
<template>
  <div class="campo">
    <label>{{ label }}</label>
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      :class="{ erro }"
    />
    <span v-if="erro" class="msg">{{ erro }}</span>
  </div>
</template>

<!-- Uso: -->
<MeuInput v-model="nome" label="Nome" :erro="erros.nome" />
<MeuInput v-model="email" label="Email" :erro="erros.email" />

Wrapper components encapsulam markup + lógica repetitiva. v-model proxy (recebe e re-emite). Props para configuração (label, erro). Slots para flexibilidade extra. Reduz duplicação em formulários. Padrão: input, select, modal, card. Manter API simples (poucas props). Compor com slots para casos especiais.

Slots avançados
<!-- Modal.vue: -->
<template>
  <div class="modal">
    <header>
      <slot name="titulo">Título padrão</slot>
      <button @click="$emit('fechar')">✕</button>
    </header>
    <main><slot /></main>
    <footer>
      <slot name="accoes">
        <button @click="$emit('fechar')">Fechar</button>
      </slot>
    </footer>
  </div>
</template>

<!-- Uso: -->
<Modal @fechar="visivel = false">
  <template #titulo><h2>Confirmar</h2></template>
  <p>Tem a certeza?</p>
  <template #accoes>
    <button @click="confirmar">Sim</button>
    <button @click="visivel = false">Não</button>
  </template>
</Modal>

Slots nomeados criam layouts flexíveis. <slot name="x"> define área; #x preenche. Slot default (sem nome) para conteúdo principal. Fallback (conteúdo dentro de slot) se não preenchido. Slots + emits = componente totalmente customizável. Padrão base de design systems (modais, cards, layouts).

Componente recursivo
<!-- ArvoreItem.vue: -->
<script setup>
defineProps({
  no: { type: Object, required: true }
});
</script>

<template>
  <li>
    <span>{{ no.nome }}</span>
    <!-- Recursão: componente usa-se a si próprio -->
    <ul v-if="no.filhos?.length">
      <ArvoreItem
        v-for="filho in no.filhos"
        :key="filho.id"
        :no="filho"
      />
    </ul>
  </li>
</template>

<!-- Uso: -->
<ul>
  <ArvoreItem :no="raiz" />
</ul>

Componentes podem referenciar-se a si próprios (recursão). Em script setup, o nome do ficheiro é o nome do componente. Essencial para árvores, menus aninhados e comentários com replies. Sempre ter condição de paragem (v-if) para evitar loop infinito. :key obrigatório no v-for recursivo.

Composition API


10 cards
Composables (use*)
// composables/useContador.js:
import { ref, computed } from "vue";

export function useContador(inicial = 0) {
  const n = ref(inicial);
  const dobro = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  function reset() { n.value = inicial; }

  return { n, dobro, inc, dec, reset };
}

// Uso:
<script setup>
import { useContador } from "@/composables/useContador";
const { n, dobro, inc, dec, reset } = useContador(10);
</script>

Composables são funções que encapsulam lógica reactiva reutilizável. Convenção: prefixo use. Retornam refs, computed e métodos. Cada chamada cria estado independente. Substituem mixins do Vue 2 (sem conflitos de nome). Podem usar lifecycle hooks internamente. Base da Composition API — organizar por funcionalidade, não por tipo.

useSlots e useAttrs
<script setup>
import { useSlots, useAttrs } from "vue";

const slots = useSlots();
const attrs = useAttrs();

// Verificar se slot existe:
const temRodape = computed(() => !!slots.rodape);

// Aceder a attrs:
console.log(attrs.class);
console.log(attrs["data-id"]);
</script>

<template>
  <div v-bind="attrs">
    <slot />
    <footer v-if="temRodape">
      <slot name="rodape" />
    </footer>
  </div>
</template>

useSlots() dá acesso programático aos slots (verificar existência, renderizar condicionalmente). useAttrs() dá acesso a atributos/eventos não declarados como props. Equivalentes a $slots e $attrs do template. Úteis em componentes wrapper que precisam de lógica baseada em slots/attrs. computed(() => !!slots.x) para condicional reactivo.

VueUse (biblioteca de composables)
// npm i @vueuse/core
<script setup>
import {
  useMouse,
  useLocalStorage,
  useDark,
  useMediaQuery,
  useEventListener,
  useDebounceFn,
  useIntersectionObserver
} from "@vueuse/core";

// Mouse position:
const { x, y } = useMouse();

// Dark mode:
const isDark = useDark();

// LocalStorage reactivo:
const token = useLocalStorage("token", "");

// Media query:
const isMobile = useMediaQuery("(max-width: 768px)");

// Debounce:
const buscaDebounced = useDebounceFn(pesquisar, 300);
</script>

VueUse é a maior biblioteca de composables Vue (200+ funções). Cobre: browser APIs, sensores, estado, animações, utilities. Todos são composables reactivos com cleanup automático. Substitui código boilerplate repetitivo. useLocalStorage, useDark, useMouse são os mais populares. Documentação excelente com demos interactivos.

Composable com lifecycle
// composables/useFetch.js:
import { ref, watchEffect } from "vue";

export function useFetch(url) {
  const dados = ref(null);
  const erro = ref(null);
  const loading = ref(true);

  async function carregar() {
    loading.value = true;
    erro.value = null;
    try {
      const res = await fetch(url.value ?? url);
      dados.value = await res.json();
    } catch (e) {
      erro.value = e.message;
    } finally {
      loading.value = false;
    }
  }

  // Se url é ref, re-fetch ao mudar:
  if (typeof url === "object") {
    watchEffect(carregar);
  } else {
    carregar();
  }

  return { dados, erro, loading, refetch: carregar };
}

Composables podem conter lifecycle hooks e watchers internamente. useFetch é o exemplo clássico: loading, erro, dados + refetch. Se a URL é ref, re-fetch automático via watchEffect. Retornar refetch para recarregar manualmente. Padrão replicável: useLocalStorage, useInterval, useMediaQuery.

watch com deep e immediate
<script setup>
import { reactive, watch } from "vue";

const form = reactive({ nome: "", email: "", morada: { rua: "", cidade: "" } });

// deep: detecta mudanças internas (nested)
watch(form, (novo) => {
  guardarRascunho(novo);
}, { deep: true });

// immediate: executa na criação (não só em mudanças)
watch(() => form.email, async (email) => {
  if (email) await verificarEmail(email);
}, { immediate: true });

// Combinar:
watch(form, handler, { deep: true, immediate: true });

// Getter como fonte (mais eficiente que deep):
watch(() => form.morada.cidade, (cidade) => {
  actualizarDistrito(cidade);
});
</script>

deep: true observa mudanças em propriedades nested (custo: percorre tudo). immediate: true executa o callback imediatamente (não espera 1ª mudança). Preferir getter (() => obj.prop) a deep quando só precisa de uma propriedade. Deep em objectos grandes é custoso — considerar watchers específicos por campo.

Testar composables
// composables/useContador.js (testável!)
export function useContador(inicial = 0) {
  const n = ref(inicial);
  const inc = () => n.value++;
  return { n, inc };
}

// useContador.spec.js (Vitest):
import { describe, it, expect } from "vitest";
import { useContador } from "./useContador";

describe("useContador", () => {
  it("inicia com valor dado", () => {
    const { n } = useContador(5);
    expect(n.value).toBe(5);
  });

  it("incrementa", () => {
    const { n, inc } = useContador(0);
    inc();
    inc();
    expect(n.value).toBe(2);
  });
});

// Executar: npx vitest

Composables são funções puras — testáveis sem componente. Importar e chamar directamente no teste. Vitest é o test runner oficial (compatível com Jest). Não precisa de mount/render para testar lógica. Para composables com lifecycle, usar @vue/test-utils com wrapper. Vantagem sobre mixins: isolamento total, sem conflitos.

Composable com cleanup
// composables/useInterval.js:
import { ref, onUnmounted } from "vue";

export function useInterval(callback, ms) {
  const activo = ref(true);
  let id = null;

  function iniciar() {
    if (id) return;
    id = setInterval(callback, ms);
    activo.value = true;
  }

  function parar() {
    clearInterval(id);
    id = null;
    activo.value = false;
  }

  iniciar();
  onUnmounted(parar);  // cleanup automático!

  return { activo, iniciar, parar };
}

// Uso:
<script setup>
import { useInterval } from "./useInterval";
const { activo, parar } = useInterval(() => {
  segundos.value++;
}, 1000);
</script>

Composables devem fazer cleanup no onUnmounted (listeners, intervals, subscriptions). Evita memory leaks quando o componente é destruído. onUnmounted dentro do composable liga ao ciclo de vida do componente que o usa. Retornar controles (parar, iniciar) para gestão manual. Padrão obrigatório para recursos externos.

Computed vs watch vs watchEffect
// computed: derivar valor (com cache)
const total = computed(() => preco.value * qtd.value);

// watch: side effect quando fonte específica muda
watch(userId, (id) => { fetchUser(id); });

// watchEffect: side effect com deps automáticas
watchEffect(() => {
  console.log(contador.value, nome.value);
  // re-executa se qualquer um mudar
});

// Regras:
// Precisa de valor derivado? → computed
// Precisa de valor antigo? → watch (novo, velho)
// Side effect simples? → watchEffect
// Fetch assíncrono? → watch (mais controle)

computed: valor derivado com cache (não executar side effects). watch: side effect com fonte explícita + acesso a valor antigo. watchEffect: side effect com dependências automáticas (executa já). computed é síncrono e cacheado; watch/watchEffect são para efeitos (fetch, DOM, logging). Escolher pela necessidade: valor → computed; efeito → watch/watchEffect.

Organização por feature
// Em vez de Options API (agrupado por tipo):
// data(), methods, computed, watch — tudo separado

// Composition API (agrupado por feature):
<script setup>
// Feature: Pesquisa
const busca = ref("");
const resultados = ref([]);
const aPesquisar = computed(() => busca.value.length > 2);
watch(busca, pesquisar);

// Feature: Paginação
const pagina = ref(1);
const totalPaginas = ref(1);
function proxima() { pagina.value++; }

// Feature: Selecção
const seleccionados = ref([]);
function toggle(item) { /* ... */ }
</script>

// Ou extrair para composables:
// const { busca, resultados } = usePesquisa();
// const { pagina, proxima } = usePaginacao();

Composition API permite agrupar código por funcionalidade (não por tipo). Tudo relacionado fica junto — mais legível em componentes grandes. Extrair para composables quando reutilizável. Contrasta com Options API onde data/methods/computed ficam separados. Escala melhor: cada feature é independente e testável.

Reactividade com Generics (TS)
<script setup lang="ts">
import { ref, computed } from "vue";

interface User {
  id: number;
  nome: string;
  email: string;
}

// Tipagem explícita:
const users = ref<User[]>([]);
const seleccionado = ref<User | null>(null);

// Inferência automática:
const contador = ref(0);  // Ref<number>
const nome = ref("Ana");  // Ref<string>

// Computed tipado:
const nomes = computed(() =>
  users.value.map(u => u.nome)
);  // ComputedRef<string[]>

// Função genérica:
function primeiro<T>(arr: T[]): T | undefined {
  return arr[0];
}
</script>

TypeScript com Vue 3: ref<T>() para tipagem explícita. Inferência automática para primitivos. computed infere tipo do retorno. Interfaces para modelos de dados. defineProps<T>() e defineEmits<T>() com generics. Volar (extensão VS Code) dá type-checking em templates. Vue 3 foi reescrito em TypeScript.

Ciclo de Vida


9 cards
Hooks (Composition API)
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from "vue";

onBeforeMount(() => {
  // Antes de renderizar (sem DOM)
});

onMounted(() => {
  // DOM disponível — aceder a elementos
});

onBeforeUpdate(() => {
  // Antes de re-renderizar
});

onUpdated(() => {
  // Após re-renderizar (DOM actualizado)
});

onBeforeUnmount(() => {
  // Antes de destruir (cleanup)
});

onUnmounted(() => {
  // Destruído — remover listeners
});
</script>

Hooks de ciclo de vida na Composition API: importar de "vue" e registar com callback. onMounted é o mais usado (DOM pronto, fetch inicial). onUnmounted para cleanup (listeners, intervals). Só funcionam dentro de setup() ou <script setup>. Múltiplos hooks do mesmo tipo são permitidos (executam por ordem).

onUnmounted — cleanup
<script setup>
import { ref, onMounted, onUnmounted } from "vue";

let intervalId = null;
const segundos = ref(0);

onMounted(() => {
  intervalId = setInterval(() => {
    segundos.value++;
  }, 1000);

  window.addEventListener("resize", aoResize);
});

onUnmounted(() => {
  clearInterval(intervalId);
  window.removeEventListener("resize", aoResize);
});

function aoResize() { /* ... */ }
</script>

onUnmounted é essencial para cleanup: intervals, timeouts, event listeners, subscriptions, WebSocket. Sem cleanup = memory leaks. Registar no onMounted, limpar no onUnmounted. Composables fazem isto automaticamente. Em SPA, componentes são destruídos ao navegar — cleanup é crítico.

Suspense (async setup)
<!-- Filho com async setup: -->
<script setup>
// Top-level await (componente assíncrono):
const dados = await fetch("/api/dados").then(r => r.json());
</script>

<!-- Pai com Suspense: -->
<template>
  <Suspense>
    <!-- Conteúdo principal (async): -->
    <template #default>
      <ComponenteAsync />
    </template>

    <!-- Fallback durante loading: -->
    <template #fallback>
      <Spinner />
    </template>
  </Suspense>

  <!-- Erro (com onErrorCaptured ou @error): -->
</template>

<Suspense> (experimental) mostra fallback enquanto componentes async carregam. Componente com top-level await no setup torna-se assíncrono. #default = conteúdo; #fallback = loading. Múltiplos filhos async: Suspense espera todos. Ainda experimental — API pode mudar. Alternativa estável: defineAsyncComponent + loading state manual.

Hooks (Options API)
<script>
export default {
  data() {
    return { dados: null };
  },

  created() {
    // Dados prontos, SEM DOM
    // Bom para: fetch inicial, init state
  },

  mounted() {
    // DOM disponível
    // Bom para: refs, medições, libs externas
    console.log(this.$el);  // elemento raiz
  },

  updated() {
    // Após re-render
  },

  unmounted() {
    // Destruído — cleanup
    clearInterval(this.timer);
  }
};
</script>

Na Options API, hooks são métodos do objecto: created, mounted, updated, unmounted. created não tem equivalente directo na Composition API (código no top-level do setup executa nessa fase). this dá acesso a dados, métodos e $el. Prefixo before para fase anterior.

activated / deactivated (KeepAlive)
<script setup>
import { onActivated, onDeactivated } from "vue";

// Só funciona dentro de <KeepAlive>:
onActivated(() => {
  // Componente reactivado (voltou ao DOM)
  console.log("Visível novamente");
  refrescarDados();
});

onDeactivated(() => {
  // Componente desactivado (saiu do DOM mas está em cache)
  console.log("Oculto (em cache)");
  pararTimer();
});
</script>

<!-- Pai: -->
<template>
  <KeepAlive>
    <component :is="vistaActual" />
  </KeepAlive>
</template>

onActivated/onDeactivated só disparam em componentes dentro de <KeepAlive>. KeepAlive mantém o componente em cache (não destrói). activated = voltou a ser visível; deactivated = foi escondido (mas existe). Útil para pausar timers, refrescar dados ao voltar. onMounted só dispara 1x com KeepAlive.

onMounted — carregar dados
<script setup>
import { ref, onMounted } from "vue";

const users = ref([]);
const loading = ref(true);
const erro = ref(null);

onMounted(async () => {
  try {
    const res = await fetch("/api/users");
    users.value = await res.json();
  } catch (e) {
    erro.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <p v-if="loading">A carregar...</p>
  <p v-else-if="erro">Erro: {{ erro }}</p>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.nome }}</li>
  </ul>
</template>

Padrão clássico: onMounted + async/await para fetch inicial. Estado triplo: loading, erro, dados. finally garante que loading acaba. Template com v-if/v-else-if/v-else para cada estado. Alternativa: composable useFetch() que encapsula tudo. Nunca fetch no top-level do setup (sem SSR safety).

onErrorCaptured
<script setup>
import { onErrorCaptured, ref } from "vue";

const erro = ref(null);

// Capturar erros de componentes filhos:
onErrorCaptured((err, instance, info) => {
  erro.value = err.message;
  console.error("Erro em:", instance?.$options.name);
  console.error("Info:", info);

  return false;  // não propagar para cima
  // return true → propaga para pai/ancestrais
});
</script>

<template>
  <div v-if="erro" class="erro">
    Algo falhou: {{ erro }}
  </div>
  <ComponenteFilho v-else />
</template>

onErrorCaptured intercepta erros de descendentes (error boundary). Recebe (erro, instância, info). return false para a propagação. Padrão: mostrar fallback UI em vez de crashar. Equivalente a Error Boundaries do React. Para erros globais: app.config.errorHandler. Info indica onde ocorreu (render, watcher, hook).

nextTick
<script setup>
import { ref, nextTick } from "vue";

const mensagem = ref("Olá");
const inputRef = ref(null);

async function actualizar() {
  mensagem.value = "Actualizado!";

  // DOM AINDA não reflecte a mudança:
  console.log(inputRef.value.textContent); // "Olá"

  // Esperar próximo tick (DOM actualizado):
  await nextTick();
  console.log(inputRef.value.textContent); // "Actualizado!"
}

// Alternativa com callback:
nextTick(() => {
  // DOM actualizado
});
</script>

Vue actualiza o DOM de forma assíncrona (batch de mudanças). nextTick() espera o próximo ciclo de renderização. Necessário quando precisa do DOM actualizado após mudar dados. Retorna Promise (usar await) ou aceita callback. Casos: focus após v-if, medições após update, scroll após inserção.

Ordem de execução
// Ciclo completo (pai + filho):

// Criação:
// 1. setup() do Pai
// 2. beforeMount do Pai
// 3. setup() do Filho
// 4. beforeMount do Filho
// 5. mounted do Filho  ← filho monta 1º
// 6. mounted do Pai

// Update:
// 1. beforeUpdate do Pai
// 2. beforeUpdate do Filho
// 3. updated do Filho
// 4. updated do Pai

// Destruição:
// 1. beforeUnmount do Pai
// 2. beforeUnmount do Filho
// 3. unmounted do Filho
// 4. unmounted do Pai

// Regra: filhos completam antes do pai

Ordem: setup → beforeMount → mounted (criação); beforeUpdate → updated (mudança); beforeUnmount → unmounted (destruição). Filhos completam antes do pai em mount/unmount. setup() executa antes de todos os hooks. Código no top-level de <script setup> = fase "created". Importante para dependências entre pai/filho.

Avançado


10 cards
Teleport
<template>
  <button @click="abrir = true">Abrir Modal</button>

  <!-- Renderiza em <body>, não aqui: -->
  <Teleport to="body">
    <div v-if="abrir" class="overlay" @click="abrir = false">
      <div class="modal" @click.stop>
        <h2>Modal</h2>
        <p>Conteúdo do modal</p>
        <button @click="abrir = false">Fechar</button>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from "vue";
const abrir = ref(false);
</script>

<style>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); }
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); }
</style>

<Teleport to="body"> renderiza conteúdo noutro local do DOM (fora da hierarquia do componente). Mantém a lógica no componente (reactividade, eventos). Ideal para modais, tooltips, dropdowns (evita overflow:hidden e z-index de ancestors). to aceita selector CSS. Múltiplos Teleports para o mesmo alvo são appendados.

v-model múltiplos
<!-- Pai: -->
<UserForm
  v-model:nome="form.nome"
  v-model:email="form.email"
  v-model:idade="form.idade"
/>

<!-- Filho (UserForm.vue): -->
<script setup>
const props = defineProps(["nome", "email", "idade"]);
const emit = defineEmits([
  "update:nome",
  "update:email",
  "update:idade"
]);
</script>

<template>
  <input :value="nome"
    @input="emit('update:nome', $event.target.value)" />
  <input :value="email"
    @input="emit('update:email', $event.target.value)" />
  <input :value="idade" type="number"
    @input="emit('update:idade', +$event.target.value)" />
</template>

Vue 3 permite múltiplos v-model nomeados num componente. Cada um é um par :prop + @update:prop. Substitui o modificador .sync do Vue 2. Nomes em camelCase no script, kebab-case no template. Ideal para formulários complexos com vários campos. Cada v-model é independente e bidireccional.

Performance e optimização
<!-- 1. v-once (renderizar 1x): -->
<p v-once>{{ conteudoEstatico }}</p>

<!-- 2. v-memo (memoizar por dependência): -->
<div v-for="item in lista" :key="item.id"
  v-memo="[item.seleccionado]">
  {{ item.nome }} — {{ item.seleccionado }}
</div>

<!-- 3. shallowRef para dados grandes: -->
<script setup>
import { shallowRef } from "vue";
const dadosGrandes = shallowRef([]);
// Substituir inteiro (não mutar nested):
dadosGrandes.value = await fetchDados();
</script>

<!-- 4. Lazy components: -->
const Pesado = defineAsyncComponent(
  () => import("./Pesado.vue")
);

<!-- 5. Virtual scroll para listas longas -->
<!-- 6. computed em vez de métodos no template -->

Optimizações Vue: v-once para conteúdo estático. v-memo para skip de re-render em listas. shallowRef para dados grandes (sem tracking profundo). defineAsyncComponent para code splitting. computed (com cache) em vez de métodos no template. Virtual scroll para +1000 itens. Vue DevTools Profiler para identificar bottlenecks.

Transition
<template>
  <Transition name="fade">
    <p v-if="visivel">Olá!</p>
  </Transition>

  <Transition name="slide">
    <div v-show="aberto" class="painel">Conteúdo</div>
  </Transition>
</template>

<style>
/* Classes geradas automaticamente: */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

.slide-enter-active, .slide-leave-active {
  transition: transform 0.3s, opacity 0.3s;
}
.slide-enter-from {
  transform: translateX(-100%);
  opacity: 0;
}
.slide-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

<Transition> aplica animações CSS quando elementos entram/saem do DOM. Classes automáticas: -enter-from, -enter-active, -enter-to, -leave-from, -leave-active, -leave-to. Prefixo = atributo name. Funciona com v-if e v-show. Para listas: <TransitionGroup>. Aceita JavaScript hooks também.

Directiva custom (global)
// main.js — registar globalmente:
app.directive("tooltip", {
  mounted(el, binding) {
    el.title = binding.value;
    el.style.cursor = "help";
  },
  updated(el, binding) {
    el.title = binding.value;
  }
});

// directivas/vClickOutside.js:
export const vClickOutside = {
  mounted(el, binding) {
    el._handler = (e) => {
      if (!el.contains(e.target)) binding.value();
    };
    document.addEventListener("click", el._handler);
  },
  unmounted(el) {
    document.removeEventListener("click", el._handler);
  }
};

// Uso:
// <div v-tooltip="'Ajuda'">?</div>
// <div v-click-outside="fechar">...</div>

Directivas globais via app.directive(). Hooks: mounted, updated, unmounted. binding.value é o valor passado. binding.arg e binding.modifiers para configuração extra. Cleanup no unmounted (remover listeners). Usar para: tooltips, click-outside, lazy-load, focus. Preferir composables quando possível.

SSR e Nuxt
// Nuxt 3 (framework SSR sobre Vue):
// npx nuxi@latest init meu-app

// Estrutura Nuxt:
// pages/       → rotas automáticas
// components/  → auto-import
// composables/ → auto-import
// server/      → API routes (Nitro)
// app.vue      → layout raiz

// pages/index.vue:
<script setup>
// useFetch: SSR-safe (executa no servidor)
const { data } = await useFetch("/api/posts");
</script>

<template>
  <div v-for="post in data" :key="post.id">
    {{ post.titulo }}
  </div>
</template>

// Vantagens: SEO, performance (FCP), code splitting

Nuxt 3 é o framework SSR/fullstack para Vue. SSR (Server-Side Rendering): HTML gerado no servidor (melhor SEO e FCP). useFetch é SSR-safe (executa no servidor + cliente). Auto-imports de componentes e composables. server/ para API routes. Alternativa SPA pura: Vite + Vue Router. Escolher Nuxt para SEO e performance.

TransitionGroup (listas)
<template>
  <TransitionGroup name="lista" tag="ul">
    <li v-for="item in itens" :key="item.id">
      {{ item.nome }}
      <button @click="remover(item.id)">✕</button>
    </li>
  </TransitionGroup>
</template>

<style>
.lista-enter-active, .lista-leave-active {
  transition: all 0.3s;
}
.lista-enter-from, .lista-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
/* Animação de movimento (FLIP): */
.lista-move {
  transition: transform 0.3s;
}
/* Necessário para leave funcionar: */
.lista-leave-active {
  position: absolute;
}
</style>

<TransitionGroup> anima inserção, remoção e reordenação de listas. tag="ul" define o elemento wrapper. :key obrigatório. Classe -move anima reposicionamento (FLIP animation). position: absolute no leave-active para não afectar layout. Ideal para listas com add/remove/reorder. Sem jQuery — tudo CSS.

Render functions e JSX
<script setup>
import { h } from "vue";

// Render function (alternativa a template):
const render = () => h("div", { class: "caixa" }, [
  h("h1", "Título"),
  h("p", { onClick: () => alert("oi") }, "Parágrafo")
]);

// Componente funcional:
function MeuTitulo(props) {
  return h("h" + props.nivel, props.texto);
}

// Com JSX (plugin @vitejs/plugin-vue-jsx):
// const Comp = () => (
//   <div class="caixa">
//     <h1>Título</h1>
//     {itens.map(i => <li key={i.id}>{i.nome}</li>)}
//   </div>
// );
</script>

h() cria VNodes programaticamente (alternativa a templates). Útil para componentes altamente dinâmicos. JSX disponível com plugin (sintaxe React-like). Templates são compilados para render functions. Preferir templates na maioria dos casos (mais legível, optimizações do compilador). Render functions para lógica complexa de renderização.

KeepAlive
<template>
  <!-- Cache de componentes dinâmicos: -->
  <KeepAlive :include="['TabA', 'TabB']" :max="5">
    <component :is="componenteActual" />
  </KeepAlive>

  <!-- include: só estes são cached -->
  <!-- exclude: estes NÃO são cached -->
  <!-- max: máximo de instâncias em cache -->
</template>

<script setup>
import { shallowRef } from "vue";
import TabA from "./TabA.vue";
import TabB from "./TabB.vue";

const componenteActual = shallowRef(TabA);
</script>

<!-- No componente cached: -->
<!-- onActivated() / onDeactivated() -->

<KeepAlive> mantém componentes em cache em vez de destruir ao trocar. Preserva estado (scroll, inputs, dados). include/exclude filtram por nome. max limita instâncias (LRU eviction). Componentes cached recebem onActivated/onDeactivated. Ideal para tabs, wizards e navegação com estado.

Error boundaries
<!-- ErrorBoundary.vue: -->
<script setup>
import { ref, onErrorCaptured } from "vue";

const erro = ref(null);
const info = ref("");

onErrorCaptured((err, instance, inf) => {
  erro.value = err;
  info.value = inf;
  return false;  // não propagar
});

function retry() {
  erro.value = null;
}
</script>

<template>
  <slot v-if="!erro" />
  <div v-else class="erro-boundary">
    <h3>Algo correu mal</h3>
    <p>{{ erro.message }}</p>
    <button @click="retry">Tentar novamente</button>
  </div>
</template>

<!-- Uso: -->
<ErrorBoundary>
  <ComponenteQuePodeFalhar />
</ErrorBoundary>

Padrão Error Boundary: componente wrapper com onErrorCaptured. Captura erros de todos os descendentes. Mostra fallback UI em vez de crashar a app. return false impede propagação. Botão retry para tentar novamente. Envolver secções críticas (widgets, conteúdo externo). Para erros globais: app.config.errorHandler.

Reatividade


11 cards
ref() — valor reactivo
<script setup>
import { ref } from "vue";

// Primitivos:
const contador = ref(0);
const nome = ref("Ana");
const activo = ref(true);

// No script: usar .value
contador.value++;
console.log(contador.value); // 1

// No template: sem .value
// {{ contador }} → 1
// {{ nome }} → "Ana"

// Arrays e objectos também:
const itens = ref([1, 2, 3]);
const user = ref({ nome: "Ana", idade: 30 });
itens.value.push(4);
user.value.idade = 31;
</script>

ref() cria um valor reactivo embrulhado num objecto com .value. No template, o .value é automático (unwrap). Funciona com qualquer tipo: primitivos, arrays, objectos. Alterar .value dispara re-render. É a API mais versátil — funciona em todo o lado (script, template, composables).

watch() — observar mudanças
<script setup>
import { ref, watch } from "vue";

const busca = ref("");
const userId = ref(1);

// Watch simples:
watch(busca, (novo, velho) => {
  console.log(`Mudou: "${velho}" → "${novo}"`);
  pesquisar(novo);
});

// Watch com opções:
watch(userId, async (id) => {
  dados.value = await fetchUser(id);
}, { immediate: true });  // executa já

// Múltiplas fontes:
watch([busca, userId], ([b, id], [bAnt, idAnt]) => {
  console.log("Algo mudou");
});

// Parar watcher:
const stop = watch(busca, fn);
stop();  // cancela
</script>

watch() executa callback quando uma fonte reactiva muda. Recebe (novo, velho). Opções: immediate: true (executa já), deep: true (mudanças internas em objectos). Aceita múltiplas fontes em array. Retorna função stop() para cancelar. Ideal para side effects: fetch, logging, sincronização. Não usar para derivar valores (usar computed).

readonly e shallowReadonly
<script setup>
import { reactive, readonly, ref } from "vue";

const original = reactive({ n: 0, lista: [1] });
const copia = readonly(original);

copia.n = 10;  // ⚠️ Warning! Não altera.
original.n = 10;  // OK → copia.n também muda

// Com ref:
const count = ref(0);
const soLeitura = readonly(count);

// Padrão: expor readonly de composable
function useContador() {
  const n = ref(0);
  const inc = () => n.value++;
  return {
    n: readonly(n),  // exterior não altera
    inc              // só via método
  };
}
</script>

readonly() cria proxy imutável — alterações disparam warning em dev. Mudanças no original reflectem no readonly (é uma vista). Padrão: composables expõem readonly() para encapsulamento (estado só muda via métodos fornecidos). shallowReadonly() só protege primeiro nível. Útil para prevenir mutações acidentais.

reactive() — objecto reactivo
<script setup>
import { reactive } from "vue";

const estado = reactive({
  nome: "Ana",
  idade: 30,
  preferencias: {
    tema: "escuro",
    idioma: "pt"
  }
});

// Sem .value:
estado.idade++;
estado.preferencias.tema = "claro";

// CUIDADO: não desestruturar (perde reactividade):
const { nome } = estado;  // ← NÃO reactivo!

// Substituir objecto inteiro (perde reactividade):
// estado = { nome: "X" };  ← ERRO!
Object.assign(estado, { nome: "X" });  // OK
</script>

reactive() torna um objecto profundamente reactivo (via Proxy). Não precisa de .value — acesso directo às propriedades. Limitações: só funciona com objectos (não primitivos); desestruturação perde reactividade; não reatribuir o objecto inteiro. Para primitivos ou necessidade de reatribuição, usar ref().

watchEffect() — efeito automático
<script setup>
import { ref, watchEffect } from "vue";

const url = ref("/api/users");
const dados = ref(null);

// Rastreia dependências automaticamente:
const stop = watchEffect(async () => {
  // url.value é rastreado como dependência
  const res = await fetch(url.value);
  dados.value = await res.json();
});

// Quando url.value mudar, re-executa

// Com cleanup:
watchEffect((onCleanup) => {
  const controller = new AbortController();
  fetch(url.value, { signal: controller.signal });

  onCleanup(() => controller.abort());
});

// Parar:
stop();
</script>

watchEffect() executa imediatamente e rastreia dependências automaticamente (sem especificar fonte). Re-executa quando qualquer reactivo usado dentro muda. onCleanup cancela efeitos anteriores (evitar race conditions em fetch). Mais simples que watch() quando não precisa de valor antigo. Retorna stop(). Ideal para efeitos colaterais reactivos.

Reactividade em profundidade
<script setup>
import { ref, isRef, unref, isReactive } from "vue";

const dados = ref({ user: { nome: "Ana" } });

// Profundamente reactivo por defeito:
dados.value.user.nome = "Maria";  // dispara render

// Verificar tipos:
isRef(dados);           // true
isReactive(dados.value); // true (objecto interno)

// unref: obter valor (ref ou não):
const val = unref(dados);  // se ref → .value, senão → próprio

// customRef: controle total
import { customRef } from "vue";
function useDebouncedRef(valor, delay = 300) {
  let timeout;
  return customRef((track, trigger) => ({
    get() { track(); return valor; },
    set(v) {
      clearTimeout(timeout);
      timeout = setTimeout(() => { valor = v; trigger(); }, delay);
    }
  }));
}
</script>

ref() e reactive() são profundamente reactivos por defeito (nested objects). isRef()/isReactive() verificam tipos. unref() extrai valor de ref ou retorna o próprio. customRef() permite controle total de tracking/triggering (ex: debounce). Vue usa Proxy (ES6) para detectar mudanças — não precisa de Vue.set() como Vue 2.

ref vs reactive
// ref: qualquer tipo, .value necessário
const n = ref(0);
const lista = ref([]);
n.value = 10;
lista.value = [1, 2, 3]; // reatribuição OK

// reactive: só objectos, sem .value
const estado = reactive({ n: 0, lista: [] });
estado.n = 10;
estado.lista = [1, 2, 3]; // OK (propriedade)

// Quando usar cada um:
// ref → primitivos, arrays, reatribuição
// reactive → formulários, estado agrupado

// Regra prática: usar ref() por defeito
// reactive() para objectos que nunca são substituídos

ref() é mais versátil: qualquer tipo, permite reatribuição, funciona em composables. reactive() é mais conveniente para objectos (sem .value) mas tem limitações. Regra prática: ref() por defeito; reactive() para formulários/estado agrupado que nunca é substituído. Ambos são profundamente reactivos.

shallowRef e shallowReactive
<script setup>
import { shallowRef, shallowReactive, triggerRef } from "vue";

// shallowRef: só .value é reactivo (não profundo)
const lista = shallowRef([1, 2, 3]);
lista.value.push(4);  // NÃO dispara render!
lista.value = [...lista.value, 4];  // OK (reatribuição)

// Forçar update manual:
triggerRef(lista);

// shallowReactive: só primeiro nível
const estado = shallowReactive({
  user: { nome: "Ana" }  // nested NÃO reactivo
});
estado.user.nome = "X";  // NÃO dispara!
estado.user = { nome: "X" };  // OK
</script>

shallowRef() / shallowReactive() são versões não-profundas — só o primeiro nível é reactivo. Performance: evitar tracking profundo em grandes estruturas. triggerRef() força update manual. Usar para dados grandes que mudam por reatribuição (listas de API, tabelas). Para nested reactividade, usar ref() normal.

Padrões de reactividade
<script setup>
import { ref, computed, watch } from "vue";

// 1. Estado + computed + watch:
const itens = ref([]);
const total = computed(() => itens.value.length);
const filtrados = computed(() =>
  itens.value.filter(i => i.ativo)
);

// 2. Loading state:
const loading = ref(false);
const erro = ref(null);
const dados = ref(null);

async function carregar() {
  loading.value = true;
  erro.value = null;
  try {
    dados.value = await fetch("/api").then(r => r.json());
  } catch (e) {
    erro.value = e.message;
  } finally {
    loading.value = false;
  }
}

// 3. Derived com side effect:
watch(filtrados, (lista) => {
  localStorage.setItem("itens", JSON.stringify(lista));
});
</script>

Padrões comuns: ref para estado, computed para derivados, watch para side effects. Loading state (loading, erro, dados) para async. Computed para filtros/transformações (com cache). Watch para persistência, logging, sync externa. Separar responsabilidades: dados → computed → effects.

computed() — valores derivados
<script setup>
import { ref, computed } from "vue";

const preco = ref(100);
const qtd = ref(3);
const desconto = ref(0.1);

// Computed (só leitura, com cache):
const total = computed(() => {
  return preco.value * qtd.value * (1 - desconto.value);
});

// Com getter e setter:
const nomeCompleto = computed({
  get: () => `${primeiro.value} ${ultimo.value}`,
  set: (val) => {
    [primeiro.value, ultimo.value] = val.split(" ");
  }
});
</script>

<template>
  <p>Total: {{ total }}€</p>
</template>

computed() deriva valores de outros reactivos com cache — só recalcula quando dependências mudam. Mais eficiente que métodos no template (que executam a cada render). Aceita getter/setter para two-way binding. Retorna um ref (usar .value no script, automático no template). Ideal para filtros, formatações e cálculos.

toRef e toRefs
<script setup>
import { reactive, toRef, toRefs } from "vue";

const estado = reactive({ nome: "Ana", idade: 30 });

// toRef: criar ref ligada a uma propriedade
const nomeRef = toRef(estado, "nome");
nomeRef.value = "Maria";  // actualiza estado.nome!

// toRefs: desestruturar mantendo reactividade
const { nome, idade } = toRefs(estado);
nome.value = "João";  // actualiza estado.nome!
idade.value = 25;     // actualiza estado.idade!

// Sem toRefs (perde ligação):
// const { nome } = estado;  ← NÃO reactivo

// Útil em composables:
function useUser(estado) {
  const { nome, idade } = toRefs(estado);
  return { nome, idade };
}
</script>

toRef() cria um ref bidireccional ligado a uma propriedade de objecto reactivo. toRefs() converte todas as propriedades em refs (para desestruturação segura). Mantêm a ligação — alterar o ref altera o objecto original. Essencial para desestruturar reactive() ou props sem perder reactividade.

Router, Pinia e Ferramentas


11 cards
Vue Router (setup)
// router/index.js:
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/HomeView.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/", component: Home },
    { path: "/about", component: () => import("../views/AboutView.vue") },
    { path: "/users/:id", component: UserDetail, name: "user" },
    { path: "/:pathMatch(.*)*", component: NotFound },
  ],
});

export default router;

// App.vue:
<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
  </nav>
  <RouterView />
</template>

createRouter() configura navegação. createWebHistory() para URLs limpos (sem #). Rotas com path e component. () => import() para lazy loading. :id para parâmetros dinâmicos. RouterLink para navegação (sem reload). RouterView renderiza o componente da rota activa. Catch-all para 404.

Pinia (acções e getters)
// stores/carrinho.js:
export const useCarrinhoStore = defineStore("carrinho", () => {
  const itens = ref([]);

  // Getter (computed):
  const total = computed(() =>
    itens.value.reduce((soma, i) => soma + i.preco * i.qtd, 0)
  );
  const totalItens = computed(() =>
    itens.value.reduce((s, i) => s + i.qtd, 0)
  );

  // Acções (funções):
  function adicionar(produto) {
    const existente = itens.value.find(i => i.id === produto.id);
    if (existente) { existente.qtd++; }
    else { itens.value.push({ ...produto, qtd: 1 }); }
  }

  function remover(id) {
    itens.value = itens.value.filter(i => i.id !== id);
  }

  async function finalizar() {
    await api.post("/pedidos", { itens: itens.value });
    itens.value = [];
  }

  return { itens, total, totalItens, adicionar, remover, finalizar };
});

Padrão de store Pinia: ref para estado, computed para getters, funções para acções. Acções podem ser async (fetch, persistência). Getters são computed (com cache). Desestruturação com storeToRefs() para manter reactividade. Múltiplas stores independentes (carrinho, auth, ui). Estado global acessível de qualquer componente.

Vue CLI vs Vite
# Vite (recomendado, actual):
npm create vue@latest
npm run dev     # instantâneo (<300ms)
npm run build   # Rollup (optimizado)

# Vue CLI (legado, Webpack):
npm install -g @vue/cli
vue create meu-app
npm run serve   # lento (30s+ em projectos grandes)

# Diferenças:
# Vite:
#   - ESM nativo (sem bundling em dev)
#   - HMR instantâneo
#   - Build com Rollup
#   - Configuração mínima

# Vue CLI:
#   - Webpack (bundling total)
#   - HMR lento em projectos grandes
#   - Mais configuração
#   - Em manutenção (não recomendado para novos)

Vite é o build tool padrão para Vue 3. Dev server com ESM nativo (sem bundling = arranque instantâneo). HMR em milissegundos. Build de produção com Rollup (tree-shaking, code-splitting). Vue CLI (Webpack) está em modo manutenção — não usar para projectos novos. Migrar de Vue CLI para Vite é recomendado.

useRoute e useRouter
<script setup>
import { useRoute, useRouter } from "vue-router";

const route = useRoute();   // info da rota actual
const router = useRouter(); // métodos de navegação

// Ler parâmetros:
const userId = route.params.id;
const busca = route.query.q;
const hash = route.hash;

// Navegação programática:
router.push("/users/42");
router.push({ name: "user", params: { id: 42 } });
router.push({ path: "/busca", query: { q: "vue" } });
router.replace("/login");  // sem history
router.back();
router.go(-2);
</script>

<template>
  <p>Rota: {{ route.path }}</p>
  <p>User: {{ route.params.id }}</p>
</template>

useRoute() dá acesso reactivo à rota actual (params, query, hash, name). useRouter() dá métodos de navegação: push (nova entrada), replace (substitui), back/go. params para :id na path; query para ?q=x. Route é reactivo — muda quando navega.

storeToRefs e $patch
<script setup>
import { storeToRefs } from "pinia";
import { useCarrinhoStore } from "@/stores/carrinho";

const store = useCarrinhoStore();

// Desestruturar mantendo reactividade:
const { total, totalItens } = storeToRefs(store);
// Métodos NÃO precisam de storeToRefs:
const { adicionar, remover } = store;

// $patch: múltiplas mudanças atómicas
store.$patch({
  itens: [],
  cupao: null
});

// $patch com função (mutações complexas):
store.$patch((state) => {
  state.itens.push(novoItem);
  state.ultimaActualizacao = Date.now();
});

// $reset (só option stores):
// store.$reset();

// $subscribe: observar mudanças
store.$subscribe((mutation, state) => {
  localStorage.setItem("carrinho", JSON.stringify(state));
});
</script>

storeToRefs() desestrutura estado/getters mantendo reactividade (métodos não precisam). $patch() aplica múltiplas mudanças de uma vez (1 re-render). $subscribe() observa mudanças para persistência. $state para substituir estado inteiro. $reset() só em option stores. Padrão: subscribe + localStorage para persistir.

Ecossistema Vue
// Oficial:
// vue-router     → navegação SPA
// pinia          → estado global
// vueuse         → composables utilitários
// vite           → build tool
// vitest         → testes unitários
// vue-devtools   → debug browser

// UI Libraries:
// Vuetify        → Material Design
// PrimeVue       → 90+ componentes
// Naive UI       → TypeScript-first
// Element Plus   → enterprise
// Quasar         → framework completo

// Ferramentas:
// @vue/test-utils → testar componentes
// vite-plugin-pwa → PWA
// unplugin-auto-import → auto-imports
// vite-plugin-pages → rotas automáticas

// Nuxt 3 → SSR/fullstack

Ecossistema Vue maduro: Router + Pinia + VueUse cobrem 90% dos casos. UI libraries: Vuetify (Material), PrimeVue (versátil), Naive UI (TS). Nuxt 3 para SSR/fullstack. Vitest + test-utils para testes. unplugin-auto-import elimina imports manuais. Comunidade activa e documentação excelente.

Rotas aninhadas e guards
// Rotas aninhadas:
{
  path: "/dashboard",
  component: DashboardLayout,
  children: [
    { path: "", component: DashHome },
    { path: "stats", component: DashStats },
    { path: "settings", component: DashSettings },
  ]
}

// DashboardLayout.vue:
// <RouterView /> renderiza child

// Navigation guards:
router.beforeEach((to, from) => {
  const auth = useAuth();
  if (to.meta.requiresAuth && !auth.logado) {
    return { path: "/login", query: { redirect: to.fullPath } };
  }
});

// Guard por rota:
{ path: "/admin", component: Admin,
  beforeEnter: (to) => { if (!isAdmin()) return "/"; }
}

children define rotas aninhadas (renderizadas no <RouterView> do pai). beforeEach é guard global (auth, permissões). beforeEnter por rota específica. meta para dados custom (requiresAuth). Retornar path para redireccionar; false para cancelar. Guards: beforeEachbeforeEnterbeforeRouteEnter (componente).

Vue DevTools
// Extensão browser: "Vue.js DevTools"
// (Chrome, Firefox, Edge)

// Funcionalidades:
// 1. Components: árvore de componentes + estado
//    - Inspecionar props, data, computed
//    - Editar valores em tempo real
//    - Ver slots e attrs

// 2. Pinia: stores + estado + histórico
//    - Time-travel (voltar a estados anteriores)
//    - Ver mutações em tempo real

// 3. Router: rotas, params, guards
//    - Histórico de navegação

// 4. Performance: timeline de renders
//    - Identificar re-renders desnecessários

// 5. Timeline: eventos, fetch, logs

// Activar em produção:
// app.config.devtools = true; (NÃO recomendado)

Vue DevTools é a extensão essencial para debug. Inspecionar árvore de componentes, props, estado reactivo. Pinia tab com time-travel debugging. Performance tab identifica renders lentos. Editar estado em tempo real para testar cenários. Só funciona em development (por defeito). Indispensável para desenvolvimento Vue.

Padrões e boas práticas
// 1. Componentes pequenos e focados
//    Máx ~200 linhas; extrair para filhos

// 2. Props down, events up
//    Dados fluem do pai; eventos sobem

// 3. Composables para lógica reutilizável
//    useAuth(), useFetch(), usePaginacao()

// 4. Nomes descritivos (multi-word)
//    AppHeader, UserCard, BaseButton

// 5. TypeScript sempre que possível
//    defineProps<T>(), defineEmits<T>()

// 6. Estado mínimo no componente
//    Global → Pinia; Local → ref/reactive

// 7. computed > métodos no template
//    Cache automático

// 8. key estável em v-for (nunca index)

// 9. Lazy load de rotas e componentes pesados

// 10. Testar composables e componentes críticos

Boas práticas Vue: componentes pequenos e focados. Props down, events up (fluxo unidireccional). Composables para lógica partilhada. TypeScript para segurança de tipos. Estado mínimo local; global em Pinia. computed com cache em vez de métodos. :key estável (nunca índice). Lazy loading para performance. Testes para lógica crítica.

Pinia (criar store)
// stores/contador.js:
import { defineStore } from "pinia";
import { ref, computed } from "vue";

// Setup store (recomendado):
export const useContadorStore = defineStore("contador", () => {
  const n = ref(0);
  const dobro = computed(() => n.value * 2);

  function inc() { n.value++; }
  function dec() { n.value--; }
  async function fetchInicial() {
    n.value = await api.getContador();
  }

  return { n, dobro, inc, dec, fetchInicial };
});

// Uso:
<script setup>
import { useContadorStore } from "@/stores/contador";
const store = useContadorStore();
</script>
<template>
  <p>{{ store.n }} (dobro: {{ store.dobro }})</p>
  <button @click="store.inc()">+1</button>
</template>

Pinia é o gestor de estado oficial do Vue (substitui Vuex). defineStore() com setup function (Composition API). Retorna refs, computed e métodos. Acesso directo: store.n, store.inc(). Estado partilhado entre componentes. Reactivo — actualiza templates automaticamente. Setup stores são mais flexíveis que option stores.

Vitest (testes unitários)
// Componente: Contador.vue
// Teste: Contador.spec.js
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Contador from "./Contador.vue";

describe("Contador", () => {
  it("mostra 0 inicialmente", () => {
    const wrapper = mount(Contador);
    expect(wrapper.text()).toContain("0");
  });

  it("incrementa ao clicar", async () => {
    const wrapper = mount(Contador);
    await wrapper.find("button").trigger("click");
    expect(wrapper.text()).toContain("1");
  });

  it("recebe props", () => {
    const wrapper = mount(Contador, {
      props: { inicial: 5 }
    });
    expect(wrapper.text()).toContain("5");
  });
});

// Executar: npx vitest

Vitest é o test runner oficial (rápido, compatível com Jest). @vue/test-utils para montar componentes. mount() renderiza o componente. find() + trigger() para interagir. await após trigger (espera DOM actualizar). Testar comportamento, não implementação. Para composables: testar directamente (sem mount).