Sobre nós Guias Projetos Contactos
Админка
please wait

O Vue.js destaca-se na criação de componentes reutilizáveis e fáceis de manter. Compreender padrões de componentes comprovados — desde props simples até composição complexa — é o que distingue código Vue apenas adequado de aplicações Vue excelentes. Este guia aborda padrões essenciais de componentes Vue na perspetiva de um programador sénior.

Porque é que os Padrões de Componentes Importam

Componentes bem concebidos permitem:

  1. Reutilização: Escrever uma vez, usar em todo o lado
  2. Testabilidade: Comportamento isolado e previsível
  3. Manutenibilidade: Responsabilidades e limites claros
  4. Escalabilidade: Padrões que crescem com a sua aplicação
  5. Colaboração em Equipa: Código consistente e fácil de compreender

Props e Eventos

Props para Baixo, Eventos para Cima

<!-- Parent.vue -->
<template>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="handleToggle"
@delete="handleDelete"
/>
</template>
<script setup>
import { ref } from 'vue';
import TodoItem from './TodoItem.vue';
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Build app', done: false }
]);
function handleToggle(id) {
const todo = todos.value.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function handleDelete(id) {
todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<!-- TodoItem.vue -->
<template>
<div class="todo-item">
<input
type="checkbox"
:checked="todo.done"
@change="$emit('toggle', todo.id)"
/>
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="$emit('delete', todo.id)">Delete</button>
</div>
</template>
<script setup>
defineProps({
todo: {
type: Object,
required: true,
validator: (todo) => {
return todo.id !== undefined &&
typeof todo.text === 'string' &&
typeof todo.done === 'boolean';
}
}
});
defineEmits(['toggle', 'delete']);
</script>
<style scoped>
.done {
text-decoration: line-through;
color: gray;
}
</style>

v-model em Componentes Personalizados

<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="$attrs"
/>
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<!-- Usage -->
<CustomInput v-model="searchQuery" placeholder="Search..." />

Múltiplas Ligações v-model

<!-- UserForm.vue -->
<template>
<div>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="First Name"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="Last Name"
/>
</div>
</template>
<script setup>
defineProps(['firstName', 'lastName']);
defineEmits(['update:firstName', 'update:lastName']);
</script>
<!-- Usage -->
<UserForm v-model:firstName="user.firstName" v-model:lastName="user.lastName" />

Slots para Composição

Slots Predefinidos e Nomeados

<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header" v-if="$slots.header">
<slot name="header" />
</div>
<div class="card-body">
<slot /> <!-- Default slot -->
</div>
<div class="card-footer" v-if="$slots.footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- Usage -->
<Card>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the main content of the card.</p>
<template #footer>
<button>Action</button>
</template>
</Card>

Scoped Slots (Padrão Render Props)

<!-- DataList.vue -->
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<slot :items="items" :refresh="fetchData" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
url: { type: String, required: true }
});
const items = ref([]);
const loading = ref(true);
const error = ref(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(props.url);
items.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
onMounted(fetchData);
</script>
<!-- Usage -->
<DataList url="/api/users" v-slot="{ items, refresh }">
<ul>
<li v-for="user in items" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="refresh">Refresh</button>
</DataList>

Composables (Composition API)

Extrair Lógica Reutilizável

// composables/useFetch.js
import { ref, watchEffect } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
watchEffect(async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url.value || url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key);
const value = ref(stored ? JSON.parse(stored) : defaultValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
// composables/useDebounce.js
import { ref, watch } from 'vue';
export function useDebounce(source, delay = 300) {
const debounced = ref(source.value);
let timeout;
watch(source, (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
debounced.value = newValue;
}, delay);
});
return debounced;
}

Utilizar Composables

<template>
<div>
<input v-model="searchTerm" placeholder="Search users..." />
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useFetch } from '@/composables/useFetch';
import { useDebounce } from '@/composables/useDebounce';
const searchTerm = ref('');
const debouncedSearch = useDebounce(searchTerm, 500);
const url = computed(() =>
debouncedSearch.value
? `/api/users?search=${debouncedSearch.value}`
: '/api/users'
);
const { data, error, loading } = useFetch(url);
</script>

Provide/Inject para Props Profundas

<!-- ThemeProvider.vue -->
<template>
<slot />
</template>
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
// Provide readonly to prevent child mutation
provide('theme', readonly(theme));
provide('toggleTheme', toggleTheme);
</script>
<!-- ThemedButton.vue (deeply nested) -->
<template>
<button
@click="toggleTheme"
:class="['btn', `btn-${theme}`]"
>
<slot />
</button>
</template>
<script setup>
import { inject } from 'vue';
const theme = inject('theme', 'light');
const toggleTheme = inject('toggleTheme', () => {});
</script>

Componentes Sem Renderização (Renderless)

<!-- MouseTracker.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
// Expose to slot
defineExpose({ x, y });
</script>
<template>
<slot :x="x" :y="y" />
</template>
<!-- Usage -->
<MouseTracker v-slot="{ x, y }">
<div>Mouse: {{ x }}, {{ y }}</div>
</MouseTracker>

Comunicação entre Componentes

Event Bus (Aplicações Pequenas)

// eventBus.js
import mitt from 'mitt';
export const bus = mitt();
// ComponentA.vue
import { bus } from '@/eventBus';
bus.emit('user-selected', { id: 1 });
// ComponentB.vue
import { bus } from '@/eventBus';
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
bus.on('user-selected', handleUserSelected);
});
onUnmounted(() => {
bus.off('user-selected', handleUserSelected);
});

Store Pinia (Aplicações Maiores)

// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
users: []
}),
getters: {
isLoggedIn: (state) => !!state.currentUser,
userById: (state) => (id) => state.users.find(u => u.id === id)
},
actions: {
async fetchUsers() {
const response = await fetch('/api/users');
this.users = await response.json();
},
async login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
this.currentUser = await response.json();
},
logout() {
this.currentUser = null;
}
}
});
<!-- Using Pinia -->
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { currentUser, isLoggedIn } = storeToRefs(userStore);
// Actions can be called directly
userStore.fetchUsers();
</script>

Componentes Dinâmicos

<template>
<component
:is="currentComponent"
v-bind="componentProps"
@action="handleAction"
/>
<button @click="switchComponent('FormA')">Form A</button>
<button @click="switchComponent('FormB')">Form B</button>
</template>
<script setup>
import { ref, computed, shallowRef, markRaw } from 'vue';
import FormA from './FormA.vue';
import FormB from './FormB.vue';
const components = {
FormA: markRaw(FormA),
FormB: markRaw(FormB)
};
const currentName = ref('FormA');
const currentComponent = computed(() => components[currentName.value]);
const componentProps = computed(() => ({
title: `This is ${currentName.value}`
}));
function switchComponent(name) {
currentName.value = name;
}
</script>

Componentes Assíncronos

import { defineAsyncComponent } from 'vue';
// Componente assíncrono básico
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
);
// Com estados de carregamento e erro
const AsyncChart = defineAsyncComponent({
loader: () => import('./components/Chart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Mostrar carregamento após 200 ms
timeout: 10000 // Timeout após 10 s
});

Principais Conclusões

  1. Props para baixo, eventos para cima: Manter um fluxo de dados unidirecional
  2. Composables para reutilização de lógica: Extrair e partilhar lógica reativa
  3. Slots para flexibilidade: Permitir que os componentes pais controlem a renderização
  4. Provide/Inject para props profundas: Evitar «prop drilling»
  5. Pinia para estado global: Quando as props dos componentes não são suficientes
  6. Componentes assíncronos para desempenho: Fazer code-splitting de componentes grandes

O sistema de componentes do Vue é notavelmente flexível — escolha padrões que correspondam à complexidade da sua aplicação, em vez de sobre-engenheirar desde o início.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive