REACT / NEXT.JS // VUE 3D::02 BAŞLANGIÇ+
20m READCOMPLETION: 84%ID::VUE-101

VUE 3 COMPOSITION API VE PINIA

Composition API, Composables, Pinia state yönetimi ve Vue Router

Vue.js 3, Composition API ile birlikte daha güçlü ve TypeScript dostu bir framework haline geldi. Hafif öğrenme eğrisi, mükemmel performansı ve esnek mimarisiyle özellikle orta ölçekli projelerde tercih edilen bir seçenektir.

Vue 3 + Vite Kurulumu

Composition API Temelleri

// VUE //
<!-- src/components/SayacBileseni.vue -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
 
// ref — reaktif primitif
const sayac = ref(0);
const isim  = ref('Ali');
 
// computed — türetilmiş reaktif değer
const ikiyeKatlanan = computed(() => sayac.value * 2);
const selamlama      = computed(() => `Merhaba, ${isim.value}!`);
 
// watch — yan etki
watch(sayac, (yeni, eski) => {
  console.log(`Sayaç ${eski}'den ${yeni}'ye değişti`);
});
 
// Lifecycle
onMounted(() => {
  console.log('Bileşen DOM\'a eklendi');
});
 
// Fonksiyonlar
function arttir() { sayac.value++ }
function sifirla() { sayac.value = 0 }
</script>
 
<template>
  <div class="sayac">
    <h2>{{ selamlama }}</h2>
    <p>Sayaç: {{ sayac }} (×2 = {{ ikiyeKatlanan }})</p>
    <button @click="arttir">+1</button>
    <button @click="sifirla">Sıfırla</button>
  </div>
</template>
 
<style scoped>
.sayac {
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
  border-radius: 0.5rem;
}
</style>

Props ve Emits

// VUE //
<!-- MakaleKart.vue -->
<script setup lang="ts">
interface Makale {
  id: string;
  baslik: string;
  excerpt: string;
  etiketler: string[];
  olusturuldu: string;
}
 
const props = defineProps<{
  makale: Makale;
  secildi?: boolean;
}>();
 
const emit = defineEmits<{
  sec:    [makaleId: string];
  sil:    [makaleId: string];
  duzenle: [makaleId: string];
}>();
 
function secimYap() {
  emit('sec', props.makale.id);
}
</script>
 
<template>
  <article
    :class="['kart', { 'kart--secildi': secildi }]"
    @click="secimYap"
  >
    <h3>{{ makale.baslik }}</h3>
    <p>{{ makale.excerpt }}</p>
    
    <div class="etiketler">
      <span
        v-for="etiket in makale.etiketler"
        :key="etiket"
        class="badge"
      >
        {{ etiket }}
      </span>
    </div>
 
    <footer>
      <time :datetime="makale.olusturuldu">
        {{ new Date(makale.olusturuldu).toLocaleDateString('tr-TR') }}
      </time>
      <button @click.stop="emit('duzenle', makale.id)">Düzenle</button>
      <button @click.stop="emit('sil', makale.id)">Sil</button>
    </footer>
  </article>
</template>

Composables — Mantığı Yeniden Kullan

// TYPESCRIPT //
// src/composables/useMakaleler.ts
import { ref, computed } from 'vue';
 
interface Makale {
  id: string;
  baslik: string;
  kategori: string;
  yayinlandi: boolean;
}
 
export function useMakaleler() {
  const makaleler = ref<Makale[]>([]);
  const yukleniyor = ref(false);
  const hata = ref<string | null>(null);
  const aramaMetni = ref('');
 
  const filtrelenmis = computed(() =>
    makaleler.value.filter(m =>
      m.baslik.toLowerCase().includes(aramaMetni.value.toLowerCase())
    )
  );
 
  async function yukle() {
    yukleniyor.value = true;
    hata.value = null;
    try {
      const res = await fetch('/api/makaleler');
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      makaleler.value = await res.json();
    } catch (e) {
      hata.value = (e as Error).message;
    } finally {
      yukleniyor.value = false;
    }
  }
 
  async function sil(id: string) {
    await fetch(`/api/makaleler/${id}`, { method: 'DELETE' });
    makaleler.value = makaleler.value.filter(m => m.id !== id);
  }
 
  return { makaleler, filtrelenmis, yukleniyor, hata, aramaMetni, yukle, sil };
}
 
// Bileşende kullanım
// MakalListesi.vue
<script setup lang="ts">
import { onMounted } from 'vue';
import { useMakaleler } from '@/composables/useMakaleler';
 
const { filtrelenmis, yukleniyor, hata, aramaMetni, yukle, sil } = useMakaleler();
onMounted(yukle);
</script>

Pinia — State Yönetimi

// TYPESCRIPT //
// src/stores/auth.store.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
 
interface Kullanici {
  id: string;
  ad: string;
  email: string;
  rol: string;
}
 
export const useAuthStore = defineStore('auth', () => {
  const kullanici = ref<Kullanici | null>(null);
  const token = ref<string | null>(localStorage.getItem('token'));
 
  const girisYapildi = computed(() => !!kullanici.value);
  const adminMi = computed(() => kullanici.value?.rol === 'admin');
 
  async function girisYap(email: string, sifre: string) {
    const res = await fetch('/api/auth/giris', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, sifre }),
    });
 
    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.hata || 'Giriş başarısız');
    }
 
    const veri = await res.json();
    token.value = veri.token;
    kullanici.value = veri.kullanici;
    localStorage.setItem('token', veri.token);
  }
 
  function cikisYap() {
    kullanici.value = null;
    token.value = null;
    localStorage.removeItem('token');
  }
 
  return { kullanici, token, girisYapildi, adminMi, girisYap, cikisYap };
});

Vue Router

// TYPESCRIPT //
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store';
 
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/',            component: () => import('@/views/AnasayfaView.vue') },
    { path: '/makaleler',   component: () => import('@/views/MakalelerView.vue') },
    { path: '/makaleler/:slug', component: () => import('@/views/MakaleDetayView.vue') },
    {
      path: '/admin',
      component: () => import('@/views/AdminView.vue'),
      meta: { gerekliRol: 'admin' },
    },
    { path: '/giris', component: () => import('@/views/GirisView.vue') },
    { path: '/:pathMatch(.*)*', component: () => import('@/views/404View.vue') },
  ],
});
 
// Navigation guard
router.beforeEach((to) => {
  const auth = useAuthStore();
 
  if (to.meta.gerekliRol && !auth.girisYapildi) {
    return { path: '/giris', query: { redirect: to.fullPath } };
  }
 
  if (to.meta.gerekliRol === 'admin' && !auth.adminMi) {
    return { path: '/', replace: true };
  }
});
 
export default router;

v-model ile Form Yönetimi

// VUE //
<!-- GirisFormu.vue -->
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useAuthStore } from '@/stores/auth.store';
import { useRouter } from 'vue-router';
 
const auth   = useAuthStore();
const router = useRouter();
 
const form = reactive({ email: '', sifre: '' });
const hata = ref('');
const yukleniyor = ref(false);
 
async function gonder() {
  if (!form.email || !form.sifre) {
    hata.value = 'Tüm alanları doldurun.';
    return;
  }
 
  yukleniyor.value = true;
  hata.value = '';
 
  try {
    await auth.girisYap(form.email, form.sifre);
    router.push('/admin');
  } catch (e) {
    hata.value = (e as Error).message;
  } finally {
    yukleniyor.value = false;
  }
}
</script>
 
<template>
  <form @submit.prevent="gonder" class="form">
    <h1>Giriş Yap</h1>
 
    <div v-if="hata" class="hata-mesaji">{{ hata }}</div>
 
    <label>
      E-posta
      <input v-model="form.email" type="email" required autocomplete="email" />
    </label>
 
    <label>
      Şifre
      <input v-model="form.sifre" type="password" required autocomplete="current-password" />
    </label>
 
    <button type="submit" :disabled="yukleniyor">
      {{ yukleniyor ? 'Giriş yapılıyor...' : 'Giriş Yap' }}
    </button>
  </form>
</template>

Sonuç

Vue 3'ün Composition API'si, mantığı composable'lar halinde ayırmanızı ve TypeScript ile tip güvenli bileşenler yazmanızı sağlar. Pinia ile merkezi state yönetimi ve Vue Router ile sayfa geçişlerini birleştirdiğinizde tam teşekküllü bir SPA altyapısı elde edersiniz. Bir sonraki derste Vue ile Nuxt.js SSR'a geçiş yapacağız.