Kurs Vue 3: komponenty jednoplikowe i props z defineProps
zweryfikowano 2 września 2026 · 5 min
Komponent to plik .vue z własnym szablonem, stanem i logiką. Zaimportuj go w <script setup>, żeby użyć go od razu w szablonie. Rodzic przekazuje mu dane przez propsy, zadeklarowane przez defineProps i podpięte z dwukropkiem.
Od siedmiu rozdziałów wszystko mieści się w jednym pliku. To się nie skaluje. Komponent to samodzielny kawałek interfejsu, z własnym szablonem, stanem i logiką, który da się wielokrotnie użyć i zagnieżdżać.
Wydzielenie karty produktu
Utwórz src/components/ProductDisplay.vue i przenieś tam wszystko, co dotyczy produktu: obrazek, tytuł, dostępność, szczegóły, kółka kolorów, przycisk.
<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>Koszyk zostaje na miejscu: nie należy do produktu. App.vue sprowadza się do:
<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>Komponent zaimportowany w <script setup> jest od razu używalny w szablonie: żadna dodatkowa deklaracja nie jest potrzebna.
Tak ma być, i o tym jest następny rozdział. cart mieszka teraz w rodzicu, poza zasięgiem komponentu potomnego. Usunęliśmy zresztą @click z przycisku: zostawienie go dałoby błąd, bo addToCart już w tym komponencie nie istnieje.
Nazywanie komponentu
Współistnieją dwie konwencje. W PascalCase, <ProductDisplay />, na pierwszy rzut oka odróżnisz komponent od znacznika HTML, a edytor potrafi przeskoczyć do jego definicji. W kebab-case, <product-display></product-display>, zachowujesz zgodność z szablonem pisanym wprost w stronie HTML.
W pliku .vue wybieraj PascalCase.
Komponent ma własny zasięg
Dodaj naraz trzy karty:
<template>
<ProductDisplay />
<ProductDisplay />
<ProductDisplay />
</template>Pojawiają się trzy niezależne karty: najechanie na kółko koloru w pierwszej nie rusza pozostałych. Każda instancja wykonuje własny <script setup> i ma własny selectedVariant.
Każda karta zachowuje też własny wybrany wariant.
To także powód, dla którego stan komponentu deklaruje się wewnątrz funkcji. Gdyby variants było zwykłym obiektem współdzielonym na poziomie modułu, trzy karty wchodziłyby sobie w drogę.
Usuń dwie nadmiarowe karty, zanim przejdziesz dalej.
Propsy
Odizolowany zasięg rodzi pytanie: jak rodzic przekazuje informację potomkowi? Przez prop, czyli własny atrybut, który komponent deklaruje jako akceptowany.
Dodajmy dostawę, darmową dla klientów premium. Informacja należy do rodzica:
<script setup>
const premium = ref(true)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" />
</template>A potomek ją deklaruje:
<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>defineProps jest dostępne bez importu w <script setup>: to makro kompilatora, a nie zwykła funkcja.
W szablonie piszesz po prostu {{ premium }}. W skrypcie trzeba przejść przez zwrócony obiekt: props.premium.
Deklarowanie propsów na poważnie
Dłuższa forma pozwala walidować:
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
}
})Wartość domyślna tablicy albo obiektu musi być zwracana przez funkcję, inaczej wszystkie instancje współdzieliłyby ten sam obiekt.
:premium="premium" przekazuje wartość zmiennej. premium="premium", bez dwukropka, przekazuje ciąg znaków "premium". Ponieważ niepusty ciąg jest prawdziwy, błąd przechodzi niezauważony aż do dnia, w którym wartość powinna być fałszywa. Vue zresztą ostrzega w konsoli, że oczekiwał typu Boolean, a dostał String.
Propsy schodzą w dół i nie wracają
Prop krąży w jedną stronę: od rodzica do potomka. Potomek nie powinien go zmieniać, jeśli spróbujesz, Vue wyświetli ostrzeżenie.
Jeśli potomek potrzebuje własnej, modyfikowalnej wersji, skopiuj ją do ref. Jeśli ma wywołać zmianę u rodzica, musi wysłać mu sygnał: o tym jest następny rozdział.
Propsy deklaruje się w opcji props, tą samą składnią walidacji, a odczytuje przez this.premium. Komponenty potomne trzeba dodatkowo zarejestrować w opcji components.
ProductDetails.vue, który dostaje tablicę w propie. Potem wyświetl dwie karty produktu i sprawdź, że każda ma faktycznie własny wybrany wariant.Częste błędy
premium="premium" przekazuje ciąg „premium”, zawsze prawdziwy. Napisz :premium="premium". Vue zresztą zgłasza oczekiwany typ Boolean, otrzymany String.