Vue 3 Composition API: ref, reactive, computed a composables
Praktický úvod do Composition API ve Vue 3 se script setup: reaktivita, computed, watch, props a emits, životní cyklus a vlastní composables.
Composition API je ve Vue 3 doporučený způsob psaní komponent. Místo rozdělení kódu do sekcí data, methods a computed (Options API) píšete logiku jako běžné funkce a proměnné – související kód je pohromadě a snadno ho vytáhnete do znovupoužitelných funkcí. S <script setup> je zápis navíc velmi stručný.
Nový projekt
npm create vue@latest
Oficiální průvodce (create-vue, postavený na Vite) se zeptá na TypeScript, Vue Router, Pinia, testy a ESLint. Pak npm install a npm run dev.
První komponenta se <script setup>
<script setup>
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">Počet: {{ count }} (dvojnásobek {{ double }})</button>
</template>
Vše, co v <script setup> deklarujete, je automaticky dostupné v šabloně. Žádné return, žádné export default.
ref vs. reactive
ref obalí libovolnou hodnotu. V JavaScriptu k ní přistupujete přes .value, v šabloně se rozbalí automaticky:
const name = ref('Jana');
name.value = 'Petr';
const user = ref({ name: 'Jana', age: 30 });
user.value.age++; // reaktivní i do hloubky
user.value = { name: 'Eva' }; // lze nahradit celý objekt
reactive funguje jen pro objekty a pole a .value nepotřebuje:
const form = reactive({ email: '', password: '' });
form.email = '[email protected]';
Má ale dvě úskalí:
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // ❌ šablona sleduje původní objekt
const { count } = reactive({ count: 0 }); // ❌ count je obyčejné číslo, reaktivita ztracena
Proto oficiální dokumentace doporučuje používat primárně ref. Pokud potřebujete destrukturovat reactive objekt, použijte toRefs(state).
computed: odvozené hodnoty
const items = ref([
{ name: 'Káva', price: 89, qty: 2 },
{ name: 'Croissant', price: 55, qty: 1 },
]);
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
);
computed se přepočítá jen když se změní hodnoty, na kterých závisí, a výsledek se cachuje. Uvnitř nedělejte vedlejší efekty (volání API, změny DOM) – na to je watch.
watch a watchEffect
import { ref, watch } from 'vue';
const query = ref('');
const results = ref([]);
watch(query, async (newQuery, _old, onCleanup) => {
if (newQuery.length < 2) { results.value = []; return; }
const controller = new AbortController();
onCleanup(() => controller.abort()); // zrušit předchozí požadavek
const res = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`, {
signal: controller.signal,
});
results.value = await res.json();
});
Užitečné volby:
{ immediate: true }– spustí se hned po vytvoření,{ deep: true }– sleduje i vnořené změny objektu,- sledovat vlastnost objektu:
watch(() => user.value.id, …)– getter, neuser.value.idpřímo.
watchEffect(() => …) spustí funkci hned a sám zjistí, na čem závisí. Je kratší, ale méně explicitní – pro složitější logiku je watch čitelnější.
Props a emits
<!-- ProductCard.vue -->
<script setup>
const props = defineProps({
name: { type: String, required: true },
price: { type: Number, required: true },
inCart: { type: Boolean, default: false },
});
const emit = defineEmits(['add']);
</script>
<template>
<article>
<h3>{{ name }}</h3>
<p>{{ price }} Kč</p>
<button :disabled="inCart" @click="emit('add', name)">Do košíku</button>
</article>
</template>
<ProductCard name="Káva" :price="89" @add="addToCart" />
S TypeScriptem lze props typovat přímo: defineProps<{ name: string; price: number }>(). Props jsou jen pro čtení – nikdy je neměňte v potomkovi, pošlete událost rodiči.
Pro obousměrné propojení (vlastní input) je od Vue 3.4 makro defineModel():
<script setup>
const model = defineModel();
</script>
<template><input v-model="model" /></template>
Životní cyklus
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
window.addEventListener('keydown', onKey);
});
onUnmounted(() => {
window.removeEventListener('keydown', onKey);
});
Odkaz na DOM prvek získáte přes ref se stejným jménem jako atribut ref v šabloně (nebo useTemplateRef('nazev') od Vue 3.5); k dispozici je až v onMounted.
Composables: znovupoužitelná logika
Composable je funkce, která používá Composition API a vrací reaktivní stav. Konvence je prefix use:
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
watchEffect(async onCleanup => {
const controller = new AbortController();
onCleanup(() => controller.abort());
loading.value = true;
error.value = null;
try {
const res = await fetch(toValue(url), { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (e) {
if (e.name !== 'AbortError') error.value = e;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
<script setup>
import { computed } from 'vue';
import { useFetch } from '@/composables/useFetch';
const props = defineProps({ userId: Number });
const { data: user, loading, error } = useFetch(
computed(() => `/api/users/${props.userId}`)
);
</script>
Díky toValue přijímá composable obyčejný řetězec, ref i getter. A protože watchEffect čte toValue(url) synchronně na začátku, při změně userId se data automaticky načtou znovu.
Hotovou sadu desítek composables (localStorage, velikost okna, schránka, online stav…) nabízí knihovna VueUse.
Sdílený stav: Pinia
Pro stav sdílený mezi komponentami (přihlášený uživatel, košík) je oficiální knihovnou Pinia. Store se dá psát stejným stylem jako komponenta:
// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const count = computed(() => items.value.length);
function add(item) { items.value.push(item); }
return { items, count, add };
});
Při destrukturování storu použijte storeToRefs(store), jinak ztratíte reaktivitu – stejné úskalí jako u reactive.
Ladění
Rozšíření Vue DevTools pro prohlížeč ukazuje strom komponent, jejich props, stav, Pinia story i časovou osu událostí. Při vývoji s Vite je k dispozici i jako plugin vite-plugin-vue-devtools přímo v aplikaci.