Skip to content

Vue.js 3 Composition API Guide — React Developer Edition

Ne Zaman Vue.js?

Kullan: Hızlı MVP, Laravel/PHP backend entegrasyonu, küçük-orta ölçekli projeler, kolay öğrenme eğrisi istenen durumlar

⚠️ Dikkat: Büyük ekiplerde React daha yaygın, iş ilanları React ağırlıklı olabilir

Kullanma: SSR gerekiyorsa direkt Vue yerine Nuxt kullan

Önerilen stack: Vue 3 + Composition API + Pinia + Vue Router + Vite

Alternatifler: React (en yaygın), Svelte (en hafif), Angular (enterprise)

Complete English + Turkish learning guide for Vue 3 with Composition API. (Vue 3 Composition API için İngilizce + Türkçe tam öğrenme rehberi.) Written for developers who already know React and want to pick up Vue fast. (React bilen geliştiriciler için hızlı Vue geçiş rehberi.)

1) Vue.js Nedir? (What is Vue.js?)

What is Vue? A progressive framework for building user interfaces. (Vue nedir? Kullanıcı arayüzleri oluşturmak için ilerlemeci bir framework.)

Unlike React (a library), Vue is a framework that includes official solutions for routing, state management, and build tooling. (React'ten farklı olarak Vue, yönlendirme, durum yönetimi ve derleme araçları için resmi çözümler içeren bir framework'tür.)

Vue vs React — Key Differences (Temel Farklar)

FeatureReactVue 3
TypeLibraryFramework
TemplateJSXHTML template (+ JSX optional)
ReactivityuseState/useReducerref / reactive (auto-tracking)
Two-way bindingManual (value + onChange)v-model (built-in)
CSS scopingCSS Modules / styled-componentsScoped CSS (built-in)
State managementRedux / ZustandPinia (official)
Routerreact-router (3rd party)vue-router (official)
Learning curveModerateGentle

Why Vue? (Neden Vue?)

  • Built-in reactivity — no need to call setState, Vue tracks changes automatically. (Yerleşik reaktivite — setState çağırmaya gerek yok, Vue değişiklikleri otomatik izler.)
  • Official ecosystem — Router, Pinia, Devtools are all first-party. (Resmi ekosistem — Router, Pinia, Devtools hepsi birinci parti.)
  • Single File Components (SFC) — template, script, style in one .vue file. (Tek Dosya Bileşenleri — template, script, style tek .vue dosyasında.)
  • Gentle migration from React — Composition API feels similar to React Hooks. (React'ten kolay geçiş — Composition API, React Hooks'a benzer hissiyat verir.)

Installation (Kurulum)

bash
# Create a new Vue 3 project with Vite
npm create vue@latest my-vue-app
# Select options: TypeScript? Router? Pinia? etc.

cd my-vue-app
npm install
npm run dev

Alternative minimal setup (Alternatif minimal kurulum):

bash
npm create vite@latest my-vue-app -- --template vue
# or with TypeScript:
npm create vite@latest my-vue-app -- --template vue-ts

Visit: http://localhost:5173


2) Proje Yapısı (Project Structure)

Vite + Vue Directory Structure (Dizin Yapısı)

my-vue-app/
  public/
    favicon.ico
  src/
    assets/            # Static assets (images, fonts)
    components/        # Reusable components (Yeniden kullanılabilir bileşenler)
    composables/       # Custom hooks / composables
    layouts/           # Layout components
    pages/ (or views/) # Route-level components (Sayfa bileşenleri)
    router/            # Vue Router config
    stores/            # Pinia stores
    App.vue            # Root component (Kök bileşen)
    main.js            # Entry point (Giriş noktası)
  index.html
  vite.config.js
  package.json

.vue File Structure — Single File Component (SFC)

Every .vue file has up to 3 sections: <script>, <template>, <style>. (Her .vue dosyası 3 bölümden oluşur: script, template, style.)

vue
<script setup>
// Composition API with <script setup> — the recommended way
// (Önerilen yöntem: <script setup> ile Composition API)
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <div>
    <h1>Counter: {{ count }}</h1>
    <button @click="increment">+1</button>
  </div>
</template>

<style scoped>
h1 {
  color: #42b883;
}
</style>

<script setup> vs Regular <script>

<script setup> is syntactic sugar — everything declared at the top level is automatically available in the template. No need for export default, setup(), or return statements. (<script setup> sözdizimsel şeker — üst düzey tanımlanan her şey template'de otomatik kullanılabilir.)

vue
<!-- Regular script (Klasik yöntem) — more verbose -->
<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}
</script>

<!-- script setup (Önerilen yöntem) — cleaner -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

main.js — Entry Point (Giriş Noktası)

javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './assets/main.css'

const app = createApp(App)

app.use(createPinia()) // State management
app.use(router)        // Router
app.mount('#app')      // Mount to DOM (DOM'a bağla)

3) Template Syntax (Şablon Sözdizimi)

Vue uses HTML-based templates with directives. React developers: think of directives as built-in JSX patterns. (Vue, direktifli HTML tabanlı template kullanır.)

Text Interpolation (Metin Yerleşimi)

vue
<template>
  <!-- Double curly braces — like {variable} in JSX -->
  <p>Hello, {{ username }}</p>
  <p>Result: {{ 2 + 3 }}</p>
  <p>{{ message.toUpperCase() }}</p>
</template>

v-bind — Dynamic Attributes (Dinamik Özellikler)

vue
<script setup>
import { ref } from 'vue'

const imageUrl = ref('/logo.png')
const isActive = ref(true)
const inputAttrs = { id: 'name', placeholder: 'Enter name' }
</script>

<template>
  <!-- v-bind:src or shorthand :src -->
  <img :src="imageUrl" :alt="'Logo'" />

  <!-- Dynamic class binding (Dinamik class bağlama) -->
  <div :class="{ active: isActive, 'text-bold': true }">Styled</div>
  <div :class="[isActive ? 'active' : '', 'base-class']">Array syntax</div>

  <!-- Dynamic style (Dinamik stil) -->
  <div :style="{ color: 'red', fontSize: '14px' }">Red text</div>

  <!-- Bind multiple attrs at once (Birden fazla özellik bağlama) -->
  <input v-bind="inputAttrs" />
</template>

v-on — Event Handling (Olay İşleme)

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

function handleClick(event) {
  console.log('Clicked!', event)
  count.value++
}
</script>

<template>
  <!-- v-on:click or shorthand @click -->
  <button @click="handleClick">Click me</button>
  <button @click="count++">Inline: {{ count }}</button>

  <!-- Event modifiers (Olay değiştiriciler) — no e.preventDefault() needed! -->
  <form @submit.prevent="handleSubmit">...</form>
  <a @click.stop="doSomething">Stop propagation</a>
  <input @keyup.enter="submitForm" />
  <button @click.once="doOnce">Runs once only</button>
</template>

React comparison: In React you call e.preventDefault() manually. Vue has .prevent, .stop, .once, .self modifiers. (React'te e.preventDefault() elle çağrılır. Vue'da .prevent, .stop gibi değiştiriciler var.)

v-if / v-else / v-show — Conditional Rendering (Koşullu Gösterim)

vue
<script setup>
import { ref } from 'vue'

const isLoggedIn = ref(false)
const role = ref('admin')
const showDetails = ref(true)
</script>

<template>
  <!-- v-if removes element from DOM (DOM'dan kaldırır) -->
  <p v-if="isLoggedIn">Welcome back!</p>
  <p v-else>Please log in.</p>

  <!-- v-else-if for chain -->
  <p v-if="role === 'admin'">Admin Panel</p>
  <p v-else-if="role === 'editor'">Editor Panel</p>
  <p v-else>User Panel</p>

  <!-- v-show toggles CSS display (CSS display değiştirir, DOM'da kalır) -->
  <!-- Use v-show for frequently toggled elements -->
  <div v-show="showDetails">Detail content here</div>
</template>

v-if vs v-show:

  • v-if — truly removes/creates elements (like {condition && <El/>} in React)
  • v-show — toggles display: none (cheaper for frequent toggling)

v-for — List Rendering (Liste Gösterimi)

vue
<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Cherry' }
])
</script>

<template>
  <!-- Always use :key with v-for (Her zaman :key kullan) -->
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>

  <!-- With index (Index ile) -->
  <div v-for="(item, index) in items" :key="item.id">
    {{ index + 1 }}. {{ item.name }}
  </div>

  <!-- Object iteration (Nesne üzerinde döngü) -->
  <div v-for="(value, key) in { name: 'Vue', version: 3 }" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- Range (Aralık) -->
  <span v-for="n in 5" :key="n">{{ n }} </span>
</template>

v-model — Two-Way Binding (Çift Yönlü Bağlama)

vue
<script setup>
import { ref } from 'vue'

const name = ref('')
const agreed = ref(false)
const selected = ref('vue')
const skills = ref([])
</script>

<template>
  <!-- Text input — React: value + onChange, Vue: just v-model -->
  <input v-model="name" placeholder="Your name" />
  <p>Hello, {{ name }}</p>

  <!-- Checkbox (Onay kutusu) -->
  <label>
    <input type="checkbox" v-model="agreed" /> I agree
  </label>

  <!-- Radio buttons -->
  <label><input type="radio" v-model="selected" value="vue" /> Vue</label>
  <label><input type="radio" v-model="selected" value="react" /> React</label>

  <!-- Multiple checkboxes bound to array (Dizi bağlama) -->
  <label><input type="checkbox" v-model="skills" value="js" /> JS</label>
  <label><input type="checkbox" v-model="skills" value="ts" /> TS</label>
  <label><input type="checkbox" v-model="skills" value="python" /> Python</label>
  <p>Selected: {{ skills }}</p>

  <!-- Modifiers (Değiştiriciler) -->
  <input v-model.trim="name" />        <!-- Auto-trim whitespace -->
  <input v-model.number="age" />        <!-- Cast to number -->
  <input v-model.lazy="search" />       <!-- Sync on change, not input -->
</template>

Computed Properties (Hesaplanmış Özellikler)

vue
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('Fahri')
const lastName = ref('Aydin')

// Like useMemo in React, but auto-tracks dependencies
// (React'teki useMemo gibi, ama bağımlılıkları otomatik izler)
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

const items = ref([1, 2, 3, 4, 5, 6])
const evenItems = computed(() => items.value.filter(n => n % 2 === 0))

// Writable computed (Yazılabilir computed)
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last
  }
})
</script>

<template>
  <p>{{ fullName }}</p>
  <p>Even: {{ evenItems }}</p>
</template>

watch & watchEffect

vue
<script setup>
import { ref, watch, watchEffect } from 'vue'

const search = ref('')
const count = ref(0)

// watch — explicit source, like useEffect with deps
// (Belirli bir kaynağı izler, useEffect + deps gibi)
watch(search, (newVal, oldVal) => {
  console.log(`Search changed: ${oldVal} -> ${newVal}`)
})

// Watch with options (Seçeneklerle izleme)
watch(search, (val) => {
  console.log('Searching:', val)
}, { immediate: true, deep: false })

// Watch multiple sources (Birden fazla kaynak izleme)
watch([search, count], ([newSearch, newCount]) => {
  console.log('Either changed:', newSearch, newCount)
})

// watchEffect — auto-tracks deps, runs immediately
// (Bağımlılıkları otomatik izler, hemen çalışır — useEffect(() => {}, [auto]) gibi)
watchEffect(() => {
  console.log('Current search:', search.value)
  // Automatically re-runs when search.value changes
})
</script>

4) Composition API

The Composition API is Vue's answer to React Hooks. It provides reactive primitives and lifecycle hooks. (Composition API, Vue'nun React Hooks'a cevabı. Reaktif ilkeller ve yaşam döngüsü kancaları sağlar.)

ref vs reactive

vue
<script setup>
import { ref, reactive } from 'vue'

// ref — wraps ANY value, access with .value in script
// (Herhangi bir değeri sarar, script'te .value ile erişim)
const count = ref(0)
const name = ref('Fahri')
const user = ref({ name: 'Fahri', age: 28 })

console.log(count.value) // 0
count.value++             // 1
user.value.name = 'Ali'   // Works

// reactive — wraps objects only, no .value needed
// (Sadece nesneleri sarar, .value gerekmez)
const state = reactive({
  count: 0,
  user: { name: 'Fahri', age: 28 }
})

console.log(state.count) // 0
state.count++             // 1
state.user.name = 'Ali'   // Works
</script>

When to use which? (Hangisini ne zaman kullanmalı?)

  • ref — for primitives (string, number, boolean) and when you might reassign the whole value
  • reactive — for objects/arrays where you won't reassign the whole object
  • Recommendation: Just use ref for everything — simpler, more consistent. (Öneri: Her şey için ref kullan — daha basit, daha tutarlı.)

Lifecycle Hooks (Yaşam Döngüsü Kancaları)

vue
<script setup>
import {
  onMounted,
  onUpdated,
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount
} from 'vue'

// React useEffect(() => { ... }, []) equivalent
onMounted(() => {
  console.log('Component mounted — DOM is ready')
  // Fetch data, add event listeners, etc.
})

// React useEffect cleanup equivalent
onUnmounted(() => {
  console.log('Component will unmount — cleanup here')
  // Remove event listeners, clear timers
})

onUpdated(() => {
  console.log('Component re-rendered')
})

onBeforeMount(() => console.log('Before mount'))
onBeforeUpdate(() => console.log('Before update'))
onBeforeUnmount(() => console.log('Before unmount'))
</script>

Lifecycle comparison (Yaşam döngüsü karşılaştırması):

ReactVue 3
useEffect(() => {}, [])onMounted()
useEffect(() => { return cleanup })onUnmounted()
useEffect(() => {}, [dep])watch(dep, callback)
No direct equivalentonUpdated()

toRef, toRefs, unref

vue
<script setup>
import { reactive, toRef, toRefs, unref, ref, isRef } from 'vue'

const state = reactive({ name: 'Fahri', age: 28 })

// toRef — create a ref linked to a reactive property
const nameRef = toRef(state, 'name')
nameRef.value = 'Ali' // Also changes state.name

// toRefs — destructure reactive without losing reactivity
// (Reaktiviteyi kaybetmeden destructure etme)
const { name, age } = toRefs(state)
name.value = 'Veli' // state.name is also 'Veli'

// unref — unwraps ref, returns value as-is if not ref
const val = ref(42)
console.log(unref(val))  // 42
console.log(unref(100))  // 100

// isRef — check if value is a ref
console.log(isRef(val))  // true
console.log(isRef(100))  // false
</script>

Composition API Derinlemesine (In Depth)

watch vs watchEffect farkı (difference):

  • watch belirli bir kaynağı açıkça izler, eski ve yeni değerlere erişim sağlar, immediate olmadan ilk çalıştırmada tetiklenmez
  • watchEffect içerisinde kullanılan tüm reaktif değerleri otomatik izler, her zaman hemen çalışır, bağımlılıkları manuel belirtmeye gerek yoktur
javascript
import { ref, watch, watchEffect } from 'vue'

const page = ref(1)
const perPage = ref(10)

// watch: sadece page değişince çalışır
watch(page, (yeni, eski) => {
  console.log(`Sayfa ${eski} -> ${yeni}`)
})

// watchEffect: hem page hem perPage değişince çalışır (otomatik izleme)
watchEffect(() => {
  fetchData(page.value, perPage.value)
})

Composable yazma kuralları (writing rules):

  • Fonksiyon adı use ile başlamalı (useCounter, useFetch)
  • İçeride ref, computed, watch gibi Composition API fonksiyonları kullanılabilir
  • Birden fazla bileşende paylaşılan mantığı kapsüllemek için idealdir
  • Composable içinde lifecycle hook kullanılabilir (onMounted, onUnmounted)
javascript
// src/composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0, step = 1) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment() { count.value += step }
  function decrement() { count.value -= step }
  function reset() { count.value = initialValue }

  return { count, doubled, increment, decrement, reset }
}

5) Components (Bileşenler)

Props (Özellikler)

vue
<!-- UserCard.vue -->
<script setup>
// defineProps — no import needed with <script setup>
// (defineProps — <script setup> ile import gerekmez)
const props = defineProps({
  name: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    default: 0
  },
  role: {
    type: String,
    default: 'user',
    validator: (value) => ['admin', 'editor', 'user'].includes(value)
  }
})

// Access in script with props.name
console.log(props.name)
</script>

<template>
  <!-- Access in template directly -->
  <div class="card">
    <h2>{{ name }}</h2>
    <p>Age: {{ age }} | Role: {{ role }}</p>
  </div>
</template>
vue
<!-- TypeScript version (TypeScript sürümü) -->
<script setup lang="ts">
interface Props {
  name: string
  age?: number
  role?: 'admin' | 'editor' | 'user'
}

const props = withDefaults(defineProps<Props>(), {
  age: 0,
  role: 'user'
})
</script>

Using the component (Bileşeni kullanma):

vue
<script setup>
import UserCard from './components/UserCard.vue'
</script>

<template>
  <UserCard name="Fahri" :age="28" role="admin" />
  <UserCard name="Ali" />  <!-- age=0, role='user' defaults -->
</template>

Emit — Child-to-Parent Communication (Çocuktan Ebeveyne İletişim)

vue
<!-- SearchInput.vue -->
<script setup>
// defineEmits — declare events this component can emit
const emit = defineEmits(['search', 'clear'])

function handleSearch(query) {
  emit('search', query) // Like calling props.onSearch(query) in React
}
</script>

<template>
  <input @input="handleSearch($event.target.value)" placeholder="Search..." />
  <button @click="emit('clear')">Clear</button>
</template>
vue
<!-- Parent usage (Ebeveyn kullanımı) -->
<script setup>
import SearchInput from './SearchInput.vue'

function onSearch(query) {
  console.log('Searching for:', query)
}
</script>

<template>
  <SearchInput @search="onSearch" @clear="() => console.log('Cleared')" />
</template>

Slots — Content Projection (İçerik Yansıtma)

Slots are like children and render props in React. (Slot'lar React'teki children ve render props gibidir.)

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <!-- Default slot — like props.children in React -->
    <slot />
  </div>
</template>
vue
<!-- Named slots (Adlandırılmış slot'lar) -->
<!-- Modal.vue -->
<template>
  <div class="modal">
    <header><slot name="header">Default Header</slot></header>
    <main><slot>Default content</slot></main>
    <footer><slot name="footer" /></footer>
  </div>
</template>
vue
<!-- Using named slots -->
<Modal>
  <template #header>
    <h2>My Modal Title</h2>
  </template>

  <p>This goes into the default slot.</p>

  <template #footer>
    <button>Close</button>
  </template>
</Modal>
vue
<!-- Scoped slots — like render props in React -->
<!-- ItemList.vue -->
<script setup>
defineProps({ items: Array })
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }}  <!-- Fallback content -->
      </slot>
    </li>
  </ul>
</template>
vue
<!-- Using scoped slots (Kapsamlı slot kullanımı) -->
<ItemList :items="products">
  <template #default="{ item }">
    <strong>{{ item.name }}</strong> — ${{ item.price }}
  </template>
</ItemList>

Provide / Inject — Dependency Injection (Bağımlılık Enjeksiyonu)

Like React Context but simpler. Parent provides, any descendant injects. (React Context gibi ama daha basit. Ebeveyn sağlar, herhangi bir torun enjekte eder.)

vue
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const toggleTheme = () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}

provide('theme', theme)             // Provide reactive value
provide('toggleTheme', toggleTheme) // Provide function
</script>
vue
<!-- DeepChild.vue — any level deep -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light')          // 'light' is fallback default
const toggleTheme = inject('toggleTheme', null)
</script>

<template>
  <div :class="theme">
    Current theme: {{ theme }}
    <button @click="toggleTheme">Toggle</button>
  </div>
</template>

Dynamic Components (Dinamik Bileşenler)

vue
<script setup>
import { ref, shallowRef } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'

const tabs = { home: TabHome, profile: TabProfile, settings: TabSettings }
const currentTab = ref('home')
</script>

<template>
  <button v-for="(_, name) in tabs" :key="name" @click="currentTab = name">
    {{ name }}
  </button>

  <!-- component :is — switches between components -->
  <KeepAlive>
    <component :is="tabs[currentTab]" />
  </KeepAlive>
</template>

<KeepAlive> caches inactive component instances (like keeping state when switching tabs). (<KeepAlive> inaktif bileşen örneklerini önbelleğeler.)

Component İleri Seviye (Advanced)

Teleport -- bileşen ağacı dışında render etme (rendering outside the component tree):

vue
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">Modal Aç</button>

  <Teleport to="body">
    <div v-if="showModal" class="modal-overlay">
      <div class="modal-content">
        <p>Bu içerik body elementine renderlanır</p>
        <button @click="showModal = false">Kapat</button>
      </div>
    </div>
  </Teleport>
</template>

Transition ile animasyonlar (animations with Transition):

vue
<template>
  <button @click="show = !show">Değiştir</button>
  <Transition name="slide-fade">
    <div v-if="show">Animasyonlu içerik</div>
  </Transition>
</template>

<style scoped>
.slide-fade-enter-active { transition: all 0.3s ease-out; }
.slide-fade-leave-active { transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); }
.slide-fade-enter-from { transform: translateX(20px); opacity: 0; }
.slide-fade-leave-to { transform: translateX(-20px); opacity: 0; }
</style>

Async Components (Asenkron Bileşenler):

javascript
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorFallback,
  delay: 200,
  timeout: 10000
})

Custom v-model birden fazla değer ile (with multiple values):

vue
<!-- DateRangePicker.vue -->
<script setup>
const props = defineProps({ startDate: String, endDate: String })
const emit = defineEmits(['update:startDate', 'update:endDate'])
</script>

<template>
  <input :value="startDate" @input="emit('update:startDate', $event.target.value)" type="date" />
  <input :value="endDate" @input="emit('update:endDate', $event.target.value)" type="date" />
</template>
vue
<!-- Kullanım (Usage) -->
<DateRangePicker v-model:startDate="baslangic" v-model:endDate="bitis" />

6) Vue Router (Yönlendirici)

Official router for Vue. Unlike React Router, it's a first-party solution. (Vue'nun resmi yönlendiricisi. React Router'dan farklı olarak birinci parti çözümdür.)

Installation (Kurulum)

bash
npm install vue-router@4

Route Configuration (Rota Tanımlama)

javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../pages/HomePage.vue') // Lazy loading
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../pages/AboutPage.vue')
    },
    {
      path: '/users/:id',          // Dynamic param (Dinamik parametre)
      name: 'user-detail',
      component: () => import('../pages/UserDetail.vue'),
      props: true                  // Pass params as props (Parametreleri prop olarak geç)
    },
    {
      path: '/dashboard',
      component: () => import('../layouts/DashboardLayout.vue'),
      meta: { requiresAuth: true },
      children: [                  // Nested routes (İç içe rotalar)
        {
          path: '',                // /dashboard
          component: () => import('../pages/DashboardHome.vue')
        },
        {
          path: 'settings',       // /dashboard/settings
          component: () => import('../pages/DashboardSettings.vue')
        }
      ]
    },
    {
      path: '/:pathMatch(.*)*',   // 404 catch-all
      name: 'not-found',
      component: () => import('../pages/NotFound.vue')
    }
  ]
})

export default router

Register in main.js

javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Using Router in Components (Bileşenlerde Router Kullanımı)

vue
<script setup>
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()     // Current route info (Mevcut rota bilgisi)
const router = useRouter()   // Navigation methods (Navigasyon metodları)

// Access params and query
console.log(route.params.id)         // /users/:id -> route.params.id
console.log(route.query.search)      // /users?search=vue -> route.query.search
console.log(route.meta.requiresAuth) // Access meta fields

// Programmatic navigation (Programatik yönlendirme)
function goToUser(id) {
  router.push(`/users/${id}`)
  // or with named route:
  router.push({ name: 'user-detail', params: { id } })
  // with query:
  router.push({ path: '/users', query: { search: 'vue' } })
}

function goBack() {
  router.back()  // Like history.back()
}
</script>

<template>
  <!-- RouterLink — like <Link> in React Router -->
  <RouterLink to="/">Home</RouterLink>
  <RouterLink :to="{ name: 'user-detail', params: { id: 1 } }">User 1</RouterLink>

  <!-- Active link styling (Aktif link stili) -->
  <RouterLink to="/about" active-class="font-bold text-blue-500">About</RouterLink>

  <!-- RouterView — renders matched component (Eşleşen bileşeni gösterir) -->
  <RouterView />
</template>
javascript
// Global guard — in router/index.js
router.beforeEach((to, from) => {
  const isAuthenticated = !!localStorage.getItem('token')

  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }
})

// Per-route guard (Rota bazlı koruyucu)
{
  path: '/admin',
  component: AdminPage,
  beforeEnter: (to, from) => {
    if (!isAdmin()) return { name: 'home' }
  }
}
vue
<!-- In-component guard (Bileşen içi koruyucu) -->
<script setup>
import { onBeforeRouteLeave } from 'vue-router'

onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    return confirm('You have unsaved changes. Leave anyway?')
  }
})
</script>

7) Pinia (State Management / Durum Yönetimi)

Pinia is Vue's official state management. Think of it as Zustand for Vue but even simpler. (Pinia, Vue'nun resmi durum yönetimi. Vue için Zustand gibi düşün ama daha basit.)

Installation (Kurulum)

bash
npm install pinia

Setup in main.js

javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Defining a Store — Composition Style (Store Tanımlama)

javascript
// src/stores/useAuthStore.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', () => {
  // State (Durum)
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || '')

  // Getters (Hesaplanmış değerler) — like computed
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name ?? 'Guest')

  // Actions (Eylemler) — like methods
  async function login(email, password) {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    })
    const data = await res.json()
    user.value = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value = null
    token.value = ''
    localStorage.removeItem('token')
  }

  return { user, token, isLoggedIn, userName, login, logout }
})

Option Style (Alternatif)

javascript
// src/stores/useCounterStore.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchCount() {
      const res = await fetch('/api/count')
      this.count = await res.json()
    }
  }
})

Using Store in Components (Bileşenlerde Store Kullanımı)

vue
<script setup>
import { useAuthStore } from '@/stores/useAuthStore'
import { useCounterStore } from '@/stores/useCounterStore'
import { storeToRefs } from 'pinia'

const authStore = useAuthStore()
const counterStore = useCounterStore()

// Use storeToRefs to destructure reactive state (keeping reactivity)
// (Reaktif durumu destructure etmek için storeToRefs kullan)
const { user, isLoggedIn, userName } = storeToRefs(authStore)

// Actions can be destructured directly (Aksiyonlar direkt destructure edilebilir)
const { login, logout } = authStore
const { increment } = counterStore
</script>

<template>
  <div v-if="isLoggedIn">
    <p>Welcome, {{ userName }}</p>
    <button @click="logout">Logout</button>
  </div>
  <div v-else>
    <button @click="login('test@test.com', '123')">Login</button>
  </div>

  <p>Count: {{ counterStore.count }} | Double: {{ counterStore.doubleCount }}</p>
  <button @click="increment">+1</button>
</template>

Pinia Detaylı (In Detail)

Pinia Plugins -- store'lara eklenti yazma (writing plugins for stores):

javascript
// src/plugins/piniaLogger.js
export function piniaLogger({ store }) {
  store.$onAction(({ name, args, after, onError }) => {
    console.log(`[Pinia] ${store.$id}.${name}`, args)
    after((result) => console.log(`[Pinia] ${name} tamamlandı`, result))
    onError((error) => console.error(`[Pinia] ${name} hata`, error))
  })
}

// main.js'de kullanım
const pinia = createPinia()
pinia.use(piniaLogger)

Pinia Persist -- state'i localStorage'a kaydetme (persisting state to localStorage):

bash
npm install pinia-plugin-persistedstate
javascript
// main.js
import piniaPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPersistedstate)
javascript
// Store'da persist aktif etme (enabling persist in store)
export const useSettingsStore = defineStore('settings', () => {
  const theme = ref('light')
  const locale = ref('tr')
  return { theme, locale }
}, {
  persist: true // otomatik olarak localStorage'a kaydeder
})

Pinia DevTools: Vue DevTools içerisinde Pinia sekmesi otomatik olarak görünür. Store'ların state, getters ve actions değerlerini gerçek zamanlı inceleyebilir, zaman yolculuğu (time-travel) yapabilirsiniz.

Store'lar arası iletişim (inter-store communication):

javascript
export const useCartStore = defineStore('cart', () => {
  const authStore = useAuthStore() // başka store'u içeride kullanabilirsiniz
  const items = ref([])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
  )

  async function checkout() {
    if (!authStore.isLoggedIn) throw new Error('Giriş yapmanız gerekli')
    await api.post('/orders', { items: items.value })
    items.value = []
  }

  return { items, total, checkout }
})

8) Form Handling (Form İşleme)

Basic Form with v-model (v-model ile Temel Form)

vue
<script setup>
import { ref, reactive } from 'vue'

const form = reactive({
  name: '',
  email: '',
  password: '',
  role: 'user',
  newsletter: false
})

const errors = ref({})
const isSubmitting = ref(false)

function validate() {
  const e = {}
  if (!form.name.trim()) e.name = 'Name is required'
  if (!form.email.includes('@')) e.email = 'Invalid email'
  if (form.password.length < 6) e.password = 'Min 6 characters'
  errors.value = e
  return Object.keys(e).length === 0
}

async function handleSubmit() {
  if (!validate()) return

  isSubmitting.value = true
  try {
    await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    })
    alert('Success!')
  } catch (err) {
    errors.value.general = 'Something went wrong'
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <label>Name</label>
      <input v-model.trim="form.name" />
      <span v-if="errors.name" class="error">{{ errors.name }}</span>
    </div>

    <div>
      <label>Email</label>
      <input v-model.trim="form.email" type="email" />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>

    <div>
      <label>Password</label>
      <input v-model="form.password" type="password" />
      <span v-if="errors.password" class="error">{{ errors.password }}</span>
    </div>

    <div>
      <label>Role</label>
      <select v-model="form.role">
        <option value="user">User</option>
        <option value="admin">Admin</option>
      </select>
    </div>

    <label>
      <input type="checkbox" v-model="form.newsletter" />
      Subscribe to newsletter
    </label>

    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? 'Submitting...' : 'Register' }}
    </button>

    <p v-if="errors.general" class="error">{{ errors.general }}</p>
  </form>
</template>

Custom Form Component with v-model (v-model ile Özel Form Bileşeni)

In Vue 3, v-model on a custom component uses modelValue prop and update:modelValue emit. (Vue 3'te özel bileşen üzerinde v-model, modelValue prop ve update:modelValue emit kullanır.)

vue
<!-- CustomInput.vue -->
<script setup>
defineProps({
  modelValue: String,
  label: String,
  error: String
})

const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <div class="form-group">
    <label v-if="label">{{ label }}</label>
    <input
      :value="modelValue"
      @input="emit('update:modelValue', $event.target.value)"
      v-bind="$attrs"
    />
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>
vue
<!-- Usage — v-model just works (v-model direkt çalışır) -->
<CustomInput v-model="form.name" label="Name" :error="errors.name" />
<CustomInput v-model="form.email" label="Email" type="email" />

Multiple v-model bindings (Birden fazla v-model bağlama)

vue
<!-- UserForm.vue -->
<script setup>
defineProps({ firstName: String, lastName: String })
defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
  <input :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>
vue
<!-- Parent usage -->
<UserForm v-model:firstName="first" v-model:lastName="last" />

9) API Çağrıları (API Calls)

Basic Data Fetching (Temel Veri Çekme)

vue
<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(true)
const error = ref(null)

async function fetchUsers() {
  loading.value = true
  error.value = null
  try {
    const res = await fetch('https://jsonplaceholder.typicode.com/users')
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    users.value = await res.json()
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

onMounted(fetchUsers)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error" class="error">Error: {{ error }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

With Axios (Axios ile)

bash
npm install axios
javascript
// src/lib/axios.js
import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
  headers: { 'Content-Type': 'application/json' }
})

// Request interceptor — attach token (İstek yakalayıcı — token ekle)
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// Response interceptor — handle errors (Yanıt yakalayıcı — hata yönetimi)
api.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status === 401) {
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(err)
  }
)

export default api
vue
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/lib/axios'

const posts = ref([])
const loading = ref(false)

async function fetchPosts() {
  loading.value = true
  try {
    const { data } = await api.get('/posts')
    posts.value = data
  } catch (err) {
    console.error('Failed to fetch posts:', err)
  } finally {
    loading.value = false
  }
}

async function createPost(title, body) {
  const { data } = await api.post('/posts', { title, body })
  posts.value.unshift(data)
}

onMounted(fetchPosts)
</script>

10) Composables (Custom Hooks)

Composables are Vue's equivalent of React custom hooks. Functions that encapsulate reactive logic. (Composable'lar Vue'nun React custom hook'larına karşılığı. Reaktif mantığı kapsülleyen fonksiyonlar.)

useFetch Composable

javascript
// src/composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(toValue(url)) // toValue handles both ref and raw
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error, loading, refetch: fetchData }
}
vue
<!-- Usage (Kullanım) -->
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data: users, loading, error, refetch } = useFetch(
  'https://jsonplaceholder.typicode.com/users'
)
</script>

<template>
  <button @click="refetch">Refresh</button>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">{{ error }}</div>
  <pre v-else>{{ users }}</pre>
</template>

useLocalStorage Composable

javascript
// src/composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return data
}
vue
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const favorites = useLocalStorage('favorites', [])
</script>

useDebounce Composable

javascript
// src/composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(source, delay = 300) {
  const debounced = ref(source.value)
  let timeout

  watch(source, (val) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debounced.value = val
    }, delay)
  })

  return debounced
}
vue
<script setup>
import { ref, watch } from 'vue'
import { useDebounce } from '@/composables/useDebounce'

const search = ref('')
const debouncedSearch = useDebounce(search, 500)

watch(debouncedSearch, (val) => {
  console.log('Debounced search:', val)
  // Trigger API call here
})
</script>

<template>
  <input v-model="search" placeholder="Search..." />
  <p>Debounced: {{ debouncedSearch }}</p>
</template>

useToggle Composable

javascript
// src/composables/useToggle.js
import { ref } from 'vue'

export function useToggle(initialValue = false) {
  const state = ref(initialValue)
  const toggle = () => { state.value = !state.value }
  const setTrue = () => { state.value = true }
  const setFalse = () => { state.value = false }

  return { state, toggle, setTrue, setFalse }
}

11) Styling (Stillendirme)

Scoped CSS (Kapsamlı CSS)

vue
<template>
  <div class="card">
    <h2>Title</h2>
    <p>Content</p>
  </div>
</template>

<style scoped>
/* Only applies to this component (Sadece bu bileşene uygulanır) */
.card {
  border: 1px solid #ccc;
  padding: 1rem;
  border-radius: 8px;
}

h2 {
  color: #42b883;
}

/* Deep selector — style child component internals */
/* (Çocuk bileşen iç stillerini değiştirme) */
:deep(.child-class) {
  color: red;
}

/* Slot content styling */
:slotted(.slot-content) {
  font-weight: bold;
}
</style>

CSS Modules

vue
<template>
  <div :class="$style.card">
    <h2 :class="$style.title">Title</h2>
  </div>
</template>

<style module>
.card {
  border: 1px solid #ccc;
  padding: 1rem;
}

.title {
  color: #42b883;
}
</style>
vue
<!-- Using useCssModule in script (Script'te kullanım) -->
<script setup>
import { useCssModule } from 'vue'
const style = useCssModule()
console.log(style.card) // Generated class name
</script>

Dynamic Styles with v-bind in CSS

vue
<script setup>
import { ref } from 'vue'

const color = ref('#42b883')
const fontSize = ref(16)
</script>

<template>
  <p class="dynamic-text">This text is dynamic!</p>
  <input v-model="color" type="color" />
  <input v-model.number="fontSize" type="range" min="12" max="36" />
</template>

<style scoped>
.dynamic-text {
  color: v-bind(color);
  font-size: v-bind(fontSize + 'px');
}
</style>

Tailwind CSS Integration (Tailwind CSS Entegrasyonu)

bash
npm install -D tailwindcss @tailwindcss/vite
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [vue(), tailwindcss()]
})
css
/* src/assets/main.css */
@import "tailwindcss";
vue
<!-- Using Tailwind in Vue (Vue'da Tailwind kullanımı) -->
<template>
  <div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
    <img :src="avatar" class="w-12 h-12 rounded-full" />
    <div>
      <h3 class="text-lg font-semibold text-gray-800">{{ name }}</h3>
      <p class="text-sm text-gray-500">{{ role }}</p>
    </div>
    <button class="ml-auto px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition">
      Follow
    </button>
  </div>
</template>

12) Build & Deployment (Derleme ve Yayınlama)

Vite Configuration (Vite Yapılandırması)

javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia']
        }
      }
    }
  }
})

Environment Variables (Ortam Değişkenleri)

bash
# .env                  — all environments
VITE_APP_TITLE=My Vue App

# .env.development      — dev only
VITE_API_URL=http://localhost:8000/api

# .env.production       — production only
VITE_API_URL=https://api.example.com
javascript
// Access in code (Kodda erişim)
// Only variables prefixed with VITE_ are exposed to client
console.log(import.meta.env.VITE_API_URL)
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.MODE)  // 'development' or 'production'
console.log(import.meta.env.DEV)   // true in dev
console.log(import.meta.env.PROD)  // true in production

Production Build (Üretim Derlemesi)

bash
# Build for production (Üretim için derleme)
npm run build

# Preview production build locally (Yerel önizleme)
npm run preview

# Analyze bundle size (Paket boyutu analizi)
npx vite-bundle-visualizer

Deployment Options (Yayın Seçenekleri)

bash
# Vercel — easiest for Vue/Vite
npm i -g vercel
vercel

# Netlify
npm run build
# Drag & drop dist/ folder or connect Git repo

# Docker
# Dockerfile
dockerfile
# Dockerfile for Vue app
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx
# nginx.conf — SPA routing support
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /assets {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

13) Vue vs React Karşılaştırma Tablosu (Comparison Table)

Quick reference table for React developers switching to Vue. (React'ten Vue'ya geçen geliştiriciler için hızlı referans tablosu.)

ConceptReactVue 3 Composition API
State (primitive)const [x, setX] = useState(0)const x = ref(0)
State (object)const [obj, setObj] = useState({})const obj = reactive({})
Update statesetX(prev => prev + 1)x.value++
Derived stateuseMemo(() => x * 2, [x])computed(() => x.value * 2)
Side effectuseEffect(() => {}, [dep])watch(dep, callback)
Mount effectuseEffect(() => {}, [])onMounted(() => {})
CleanupuseEffect(() => { return cleanup })onUnmounted(cleanup)
Auto-effectN/AwatchEffect(() => {})
Propsfunction Comp({ name })defineProps({ name: String })
Eventsprops.onClick()emit('click')
Childrenprops.children<slot />
Render propsprops.render(data)<slot :data="data" /> (scoped slot)
ContextcreateContext + useContextprovide + inject
Ref to DOMuseRef(null)const el = ref(null) + ref="el"
Conditional{show && <Comp/>}<Comp v-if="show" />
List renderarr.map(x => <li key={x.id}>)<li v-for="x in arr" :key="x.id">
Two-way bindvalue={x} onChange={e => setX(e)}v-model="x"
CSS scopeCSS Modules / styled-components<style scoped>
Custom hookuseXxx()useXxx() (composable)
State mgmtRedux / ZustandPinia
Routerreact-routervue-router
Lazy loadReact.lazy(() => import(...))defineAsyncComponent(() => import(...))
PortalcreatePortal(el, target)<Teleport to="target">
Error boundary<ErrorBoundary>onErrorCaptured()
Suspense<Suspense fallback={...}><Suspense> + #fallback slot
Fragment<>...</>Multiple root elements (built-in)
Dynamic compConditional rendering<component :is="comp" />

Common Patterns Side-by-Side (Yaygın Kalıplar Karşılaştırmalı)

Counter Example (Sayaç Örneği):

vue
<!-- Vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">Count: {{ count }}</button>
</template>
javascript
// React equivalent
import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}

Data Fetching (Veri Çekme):

vue
<!-- Vue -->
<script setup>
import { ref, onMounted } from 'vue'

const data = ref(null)
const loading = ref(true)

onMounted(async () => {
  data.value = await fetch('/api/data').then(r => r.json())
  loading.value = false
})
</script>

<template>
  <div v-if="loading">Loading...</div>
  <pre v-else>{{ data }}</pre>
</template>
javascript
// React equivalent
import { useState, useEffect } from 'react'

export default function DataFetcher() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/data')
      .then(r => r.json())
      .then(d => { setData(d); setLoading(false) })
  }, [])

  if (loading) return <div>Loading...</div>
  return <pre>{JSON.stringify(data)}</pre>
}

14) Sık Kullanılan Komutlar (Frequently Used Commands)

Project Commands (Proje Komutları)

bash
# Create project (Proje oluştur)
npm create vue@latest my-app

# Run dev server (Geliştirme sunucusu)
npm run dev

# Build for production (Üretim derlemesi)
npm run build

# Preview production build (Üretim önizleme)
npm run preview

# Type check (Tip kontrolü — TypeScript)
npm run type-check

# Lint (Kod kalite kontrolü)
npm run lint

Package Installation (Paket Kurulumu)

bash
# Core packages (Temel paketler)
npm install vue-router@4          # Router
npm install pinia                 # State management
npm install axios                 # HTTP client

# Dev tools (Geliştirme araçları)
npm install -D @vue/devtools      # Vue DevTools
npm install -D vite-plugin-vue-devtools

# UI Libraries (Arayüz kütüphaneleri)
npm install primevue              # PrimeVue
npm install vuetify               # Vuetify
npm install @headlessui/vue       # Headless UI

# Form validation (Form doğrulama)
npm install vee-validate          # VeeValidate
npm install @vuelidate/core @vuelidate/validators  # Vuelidate
npm install zod                   # Schema validation

# Styling
npm install -D tailwindcss @tailwindcss/vite

# Testing (Test)
npm install -D vitest @vue/test-utils  # Unit tests
npm install -D cypress                  # E2E tests
npm install -D @testing-library/vue     # Testing Library

Vue CLI Commands (Vue CLI Komutları)

bash
# Vue DevTools — install browser extension
# Chrome: Vue.js devtools extension
# Firefox: Vue.js devtools extension

# Vite plugins (Vite eklentileri)
npm install -D unplugin-auto-import    # Auto-import Vue APIs
npm install -D unplugin-vue-components # Auto-import components

Useful Code Snippets (Faydalı Kod Parçaları)

vue
<!-- Teleport — render outside component tree (Bileşen ağacının dışında göster) -->
<!-- Like React's createPortal -->
<Teleport to="body">
  <div class="modal-overlay">
    <div class="modal">Modal content</div>
  </div>
</Teleport>

<!-- Suspense — async component loading -->
<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    <div>Loading...</div>
  </template>
</Suspense>

<!-- Transition — built-in animations (Yerleşik animasyonlar) -->
<Transition name="fade">
  <div v-if="show">Animated content</div>
</Transition>
css
/* Transition CSS classes */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
  opacity: 0;
}
vue
<!-- TransitionGroup — list animations (Liste animasyonları) -->
<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>

Template Refs — Direct DOM Access (Doğrudan DOM Erişimi)

vue
<script setup>
import { ref, onMounted } from 'vue'

const inputRef = ref(null) // Same name as template ref attribute

onMounted(() => {
  inputRef.value.focus() // Direct DOM access, like useRef in React
})
</script>

<template>
  <input ref="inputRef" placeholder="Auto-focused" />
</template>

defineAsyncComponent — Lazy Loading Components

javascript
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// With loading/error states (Yükleme/hata durumları ile)
const AsyncModal = defineAsyncComponent({
  loader: () => import('./components/Modal.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,     // Delay before showing loading (ms)
  timeout: 3000   // Timeout before showing error (ms)
})

15) Nuxt 3 Tanıtım (Introduction)

Nuxt, Vue için full-stack framework'tür. SSR, SSG, file-based routing ve server API gibi özellikler sunar.

Nuxt 3 Kurulum (Installation)

bash
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev

Nuxt 3 Dizin Yapısı (Directory Structure)

my-nuxt-app/
  pages/             # File-based routing (dosya tabanlı yönlendirme)
    index.vue        # -> /
    about.vue        # -> /about
    users/
      [id].vue       # -> /users/:id (dinamik rota)
  components/        # Otomatik import edilen bileşenler
  composables/       # Otomatik import edilen composable'lar
  layouts/           # Sayfa layout'ları
  server/
    api/             # Server API route'ları
      hello.ts       # -> /api/hello
  middleware/         # Route middleware'leri
  plugins/           # Nuxt plugin'leri
  nuxt.config.ts     # Nuxt yapılandırması
  app.vue            # Root component

File-Based Routing

Nuxt, pages/ klasöründeki dosya yapısına göre otomatik rota oluşturur:

pages/
  index.vue          -> /
  about.vue          -> /about
  blog/
    index.vue        -> /blog
    [slug].vue       -> /blog/:slug
  users/
    [id]/
      index.vue      -> /users/:id
      settings.vue   -> /users/:id/settings

useFetch ve useAsyncData

vue
<script setup>
// useFetch -- URL'den veri çekme (SSR uyumlu)
const { data: posts, pending, error } = await useFetch('/api/posts')

// useAsyncData -- daha esnek veri çekme (more flexible data fetching)
const { data: user } = await useAsyncData('user', () => {
  return $fetch(`/api/users/${route.params.id}`)
})
</script>

<template>
  <div v-if="pending">Yükleniyor...</div>
  <div v-else-if="error">Hata: {{ error.message }}</div>
  <div v-else>
    <div v-for="post in posts" :key="post.id">{{ post.title }}</div>
  </div>
</template>

Server API Routes

typescript
// server/api/hello.ts
export default defineEventHandler((event) => {
  return { message: 'Merhaba Nuxt!' }
})

// server/api/users/[id].ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')
  return { id, name: `Kullanıcı ${id}` }
})

Nuxt vs Next.js Karşılaştırma (Comparison)

Özellik (Feature)Nuxt 3 (Vue)Next.js (React)
FrameworkVue 3React
RoutingFile-based (otomatik)File-based (otomatik)
SSRVarsayılan (default)Varsayılan (default)
SSGnuxt generatenext export / static
API Routesserver/api/app/api/
Veri çekme (data fetching)useFetch, useAsyncDatafetch (Server Components)
Meta frameworkNitro (sunucu motoru)Vercel altyapısı
Öğrenme eğrisi (learning curve)Vue biliyorsan kolayReact biliyorsan kolay

16) Güvenlik (Security)

v-html ve XSS Riski (XSS Risk)

v-html kullanıcıdan gelen veriyi doğrudan DOM'a basar, bu XSS saldırısı riskine yol açar:

vue
<!-- TEHLİKELİ: Kullanıcı girdisini doğrudan v-html ile gösterme -->
<div v-html="userInput"></div>

<!-- GÜVENLİ: Text interpolation XSS'e karşı güvenlidir -->
<div>{{ userInput }}</div>

Eğer v-html kullanmanız gerekiyorsa, girdiyi sanitize edin:

bash
npm install dompurify
javascript
import DOMPurify from 'dompurify'

const safeHtml = computed(() => DOMPurify.sanitize(rawHtml.value))

Auth Guard ile Rota Koruma (Route Protection with Auth Guard)

javascript
// router/index.js
router.beforeEach((to, from) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isLoggedIn) {
    return { name: 'login', query: { redirect: to.fullPath } }
  }

  if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') {
    return { name: 'forbidden' }
  }
})

Input Sanitization (Girdi Temizleme)

javascript
// Kullanıcı girdilerini her zaman doğrulayın (always validate user inputs)
function sanitizeInput(input) {
  return input
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

// Form doğrulamada zod kullanımı (using zod for form validation)
import { z } from 'zod'

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().min(0).max(150)
})

CSP (Content Security Policy)

html
<!-- index.html veya sunucu tarafında header olarak -->
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';">

17) Test (Testing — Vitest + Vue Test Utils)

Kurulum (Installation)

bash
npm install -D vitest @vue/test-utils jsdom
javascript
// vite.config.js
export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true
  }
})

Temel Bileşen Testi (Basic Component Test)

javascript
// src/components/__tests__/Counter.test.js
import { describe, it, expect } from 'vitest'
import { mount, shallowMount } from '@vue/test-utils'
import Counter from '../Counter.vue'

describe('Counter', () => {
  // mount: tüm alt bileşenleri de renderlar
  // shallowMount: alt bileşenleri stub'lar, sadece hedef bileşeni test eder
  it('başlangıç değerini gösterir', () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('0')
  })

  it('butona tıklanınca sayacı arttırır', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })

  it('props ile başlangıç değeri alır', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 10 }
    })
    expect(wrapper.text()).toContain('10')
  })
})

Event Testi (Event Test)

javascript
it('search event emit eder', async () => {
  const wrapper = mount(SearchInput)
  const input = wrapper.find('input')

  await input.setValue('vue')
  await input.trigger('input')

  expect(wrapper.emitted('search')).toBeTruthy()
  expect(wrapper.emitted('search')[0]).toEqual(['vue'])
})

Pinia Store Testi (Pinia Store Test)

javascript
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/useCounterStore'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('başlangıç state doğru', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)
  })

  it('increment çalışır', () => {
    const store = useCounterStore()
    store.increment()
    expect(store.count).toBe(1)
  })

  it('doubleCount getter doğru hesaplar', () => {
    const store = useCounterStore()
    store.count = 5
    expect(store.doubleCount).toBe(10)
  })
})

18) İpuçları ve En İyi Pratikler (Tips and Best Practices)

  • Composition API tercih edin: Options API hâlâ destekleniyor ama yeni projelerde Composition API + <script setup> standart
  • Basit değerler için ref, nesne grupları için reactive kullanın: ref her yerde çalışır ve daha tahmin edilebilir
  • computed kullanın, method yerine: computed değerleri önbelleğeler, method her renderda yeniden çalışır
  • toRefs ile reactive destructure edin: const { name } = toRefs(state) reaktiviteyi korur, direkt destructure reaktiviteyi kırar
  • defineProps ve defineEmits kullanın: <script setup> içerisinde tip güvenli props ve events tanımlayın
  • Store'larda storeToRefs kullanın: Pinia store'dan state/getters çıkarırken storeToRefs kullanmazsanız reaktivite kaybolur
  • Büyük bileşenleri composable'lara bölün: 200+ satırlık bileşenler yerine mantığı use fonksiyonlarına çıkartın
  • watchEffect basit izleme, watch detaylı kontrol için: watchEffect otomatik izler, watch eski/yeni değer karşılaştırması ve lazy çalıştırma sunar

Tip: Vue's Composition API and React Hooks share the same mental model: encapsulate stateful logic in functions. If you know React Hooks, you already understand 80% of Vue Composition API. The main differences are: Vue's reactivity is automatic (no dependency arrays), v-model gives you two-way binding for free, and Vue's template syntax with directives replaces JSX conditional/loop patterns.

(İpucu: Vue Composition API ve React Hooks aynı zihinsel modeli paylaşıyor. React Hooks biliyorsanız, Vue Composition API'nin %80'ini zaten anlıyorsunuz. Ana farklar: Vue'nun reaktivitesi otomatik, v-model iki yönlü bağlama sağlar ve Vue'nun template sözdizimi JSX kalıplarının yerini alır.)


Frontend

Diğer Kategoriler (Other Categories)

Developer Guides & Technical References