Programování

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.

12. 10. 2026 · 5 min čtení

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, ne user.value.id pří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.