Tutorial VueJS 3 – Componentes e props com defineProps
verificado a 2 Setembro 2026 · 5 min
Um componente é um ficheiro .vue com o seu template, o seu estado e a sua lógica. Importa-o dentro de <script setup> para o usares diretamente no template. O pai passa-lhe dados através de props, declarados com defineProps e ligados com os dois-pontos.
Há sete capítulos que tudo cabe num único ficheiro. Isso não escala. Um componente é um pedaço de interface autónomo, com o seu template, o seu estado e a sua lógica, que se reutiliza e se encaixa dentro de outros.
Extrair a ficha do produto
Cria src/components/ProductDisplay.vue e passa para lá tudo o que diz respeito ao produto: a imagem, o título, a disponibilidade, os detalhes, as pastilhas e o botão.
<script setup>
import { computed, ref } from 'vue'
const product = ref('T-Shirt')
const brand = ref('Gekkode')
const selectedVariant = ref(0)
const details = ref(['60 % coton', '30 % laine', '10 % polyester'])
const variants = ref([
{ id: 2234, color: '#2563eb', image: '/images/t-shirt-bleu.svg', quantity: 20 },
{ id: 2235, color: '#dc2626', image: '/images/t-shirt-rouge.svg', quantity: 0 }
])
const title = computed(() => `${brand.value} ${product.value}`)
const image = computed(() => variants.value[selectedVariant.value].image)
const inStock = computed(() => variants.value[selectedVariant.value].quantity > 0)
function updateVariant(index) {
selectedVariant.value = index
}
</script>
<template>
<div class="product-display">
<div class="product-image">
<img :src="image" :alt="title">
</div>
<div class="product-info">
<h1>{{ title }}</h1>
<p v-if="inStock">En stock</p>
<p v-else>En rupture</p>
<ul>
<li v-for="detail in details" :key="detail">{{ detail }}</li>
</ul>
<div class="variants-wrapper">
<div
v-for="(variant, index) in variants"
:key="variant.id"
class="color-circle"
:style="{ backgroundColor: variant.color }"
@mouseover="updateVariant(index)"
></div>
</div>
<button class="button" :class="{ disabledButton: !inStock }" :disabled="!inStock">
Ajouter au panier
</button>
</div>
</div>
</template>O carrinho não vai com ele: não pertence ao produto. O App.vue reduz-se a:
<script setup>
import { ref } from 'vue'
import ProductDisplay from './components/ProductDisplay.vue'
const cart = ref(0)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay />
</template>Um componente importado dentro de <script setup> fica logo utilizável no template: não é preciso mais nenhuma declaração.
É normal, e é o tema do capítulo seguinte. O cart vive agora no pai, fora do alcance do componente filho. Aliás, retirámos o @click do botão: deixá-lo teria dado erro, já que addToCart deixou de existir neste componente.
Dar nome a um componente
Coexistem duas convenções. Em PascalCase, <ProductDisplay />, distingues num relance um componente de uma tag HTML, e o editor consegue saltar para a definição dele. Em kebab-case, <product-display></product-display>, mantém-se a compatibilidade com um template escrito diretamente numa página HTML.
Num ficheiro .vue, prefere o PascalCase.
Um componente tem o seu próprio âmbito
Acrescenta três fichas de uma vez:
<template>
<ProductDisplay />
<ProductDisplay />
<ProductDisplay />
</template>Aparecem três fichas, independentes: passar o rato por uma pastilha da primeira não mexe nas outras. Cada instância executa o seu próprio <script setup> e tem o seu próprio selectedVariant.
Cada ficha guarda também a sua própria variante selecionada.
É também por isso que o estado de um componente se declara dentro de uma função. Se variants fosse um simples objeto partilhado ao nível do módulo, as três fichas pisavam-se umas às outras.
Retira as duas fichas a mais antes de continuar.
Os props
O âmbito isolado levanta uma questão: como é que o pai transmite uma informação ao filho? Através de um prop, um atributo personalizado que o componente declara aceitar.
Vamos acrescentar os portes, gratuitos para os clientes premium. A informação pertence ao pai:
<script setup>
const premium = ref(true)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" />
</template>E o filho declara-a:
<script setup>
const props = defineProps({
premium: { type: Boolean, required: true }
})
const shipping = computed(() => (props.premium ? 'Gratuite' : '2,99 €'))
</script>
<template>
<p>Livraison : {{ shipping }}</p>
</template>O defineProps está disponível sem import dentro de <script setup>: é uma macro do compilador, não uma função vulgar.
No template escreve-se {{ premium }} diretamente. No script, é preciso passar pelo objeto devolvido: props.premium.
Declarar os props a sério
A forma longa permite validar:
const props = defineProps({
premium: { type: Boolean, required: true },
titre: { type: String, default: 'Sans titre' },
tailles: { type: Array, default: () => [] },
note: {
type: Number,
validator: (valeur) => valeur >= 0 && valeur <= 5
}
})O valor por omissão de um array ou de um objeto tem de ser devolvido por uma função, senão todas as instâncias partilhariam o mesmo objeto.
:premium="premium" transmite o valor da variável. premium="premium", sem dois-pontos, transmite a string "premium". Como uma string não vazia é verdadeira, o bug passa despercebido até ao dia em que o valor devia ser falso. Aliás, o Vue avisa na consola de um tipo esperado Boolean e recebido String.
Os props descem, não sobem
Um prop circula num só sentido: do pai para o filho. O filho não o deve modificar, o Vue mostra um aviso se tentares.
Se o filho precisar de uma versão local modificável, copia-a para um ref. Se tiver de provocar uma mudança no pai, tem de lhe enviar um sinal: é esse o tema do capítulo seguinte.
Os props declaram-se na opção props, com a mesma sintaxe de validação, e leem-se por this.premium. Além disso, os componentes filhos têm de ser registados na opção components.
ProductDetails.vue que recebe o array como prop. Depois mostra duas fichas de produto e confirma que cada uma tem mesmo a sua própria variante selecionada.Erros frequentes
premium="premium" transmite a string «premium», sempre verdadeira. Escreve :premium="premium". Aliás, o Vue assinala um tipo Boolean esperado e String recebido.