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)
| Feature | React | Vue 3 |
|---|---|---|
| Type | Library | Framework |
| Template | JSX | HTML template (+ JSX optional) |
| Reactivity | useState/useReducer | ref / reactive (auto-tracking) |
| Two-way binding | Manual (value + onChange) | v-model (built-in) |
| CSS scoping | CSS Modules / styled-components | Scoped CSS (built-in) |
| State management | Redux / Zustand | Pinia (official) |
| Router | react-router (3rd party) | vue-router (official) |
| Learning curve | Moderate | Gentle |
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
.vuefile. (Tek Dosya Bileşenleri — template, script, style tek.vuedosyası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)
# 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 devAlternative minimal setup (Alternatif minimal kurulum):
npm create vite@latest my-vue-app -- --template vue
# or with TypeScript:
npm create vite@latest my-vue-app -- --template vue-tsVisit: 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.)
<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.)
<!-- 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ı)
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)
<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)
<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)
<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)
<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— togglesdisplay: none(cheaper for frequent toggling)
v-for — List Rendering (Liste Gösterimi)
<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)
<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)
<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
<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
<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 valuereactive— for objects/arrays where you won't reassign the whole object- Recommendation: Just use
reffor everything — simpler, more consistent. (Öneri: Her şey içinrefkullan — daha basit, daha tutarlı.)
Lifecycle Hooks (Yaşam Döngüsü Kancaları)
<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ı):
| React | Vue 3 |
|---|---|
useEffect(() => {}, []) | onMounted() |
useEffect(() => { return cleanup }) | onUnmounted() |
useEffect(() => {}, [dep]) | watch(dep, callback) |
| No direct equivalent | onUpdated() |
toRef, toRefs, unref
<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):
watchbelirli bir kaynağı açıkça izler, eski ve yeni değerlere erişim sağlar,immediateolmadan ilk çalıştırmada tetiklenmezwatchEffectiçerisinde kullanılan tüm reaktif değerleri otomatik izler, her zaman hemen çalışır, bağımlılıkları manuel belirtmeye gerek yoktur
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ı
useile 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)
// 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)
<!-- 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><!-- 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):
<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)
<!-- 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><!-- 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.)
<!-- Card.vue -->
<template>
<div class="card">
<!-- Default slot — like props.children in React -->
<slot />
</div>
</template><!-- 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><!-- 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><!-- 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><!-- 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.)
<!-- 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><!-- 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)
<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):
<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):
<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):
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):
<!-- 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><!-- 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)
npm install vue-router@4Route Configuration (Rota Tanımlama)
// 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 routerRegister in main.js
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ı)
<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>Navigation Guards (Navigasyon Koruyucuları)
// 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' }
}
}<!-- 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)
npm install piniaSetup in main.js
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)
// 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)
// 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ı)
<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):
// 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):
npm install pinia-plugin-persistedstate// main.js
import piniaPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPersistedstate)// 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):
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)
<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.)
<!-- 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><!-- 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)
<!-- 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><!-- Parent usage -->
<UserForm v-model:firstName="first" v-model:lastName="last" />9) API Çağrıları (API Calls)
Basic Data Fetching (Temel Veri Çekme)
<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)
npm install axios// 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<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
// 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 }
}<!-- 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
// 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
}<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
const favorites = useLocalStorage('favorites', [])
</script>useDebounce Composable
// 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
}<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
// 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)
<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
<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><!-- 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
<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)
npm install -D tailwindcss @tailwindcss/vite// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [vue(), tailwindcss()]
})/* src/assets/main.css */
@import "tailwindcss";<!-- 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ı)
// 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)
# .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// 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 productionProduction Build (Üretim Derlemesi)
# 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-visualizerDeployment Options (Yayın Seçenekleri)
# 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 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.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.)
| Concept | React | Vue 3 Composition API |
|---|---|---|
| State (primitive) | const [x, setX] = useState(0) | const x = ref(0) |
| State (object) | const [obj, setObj] = useState({}) | const obj = reactive({}) |
| Update state | setX(prev => prev + 1) | x.value++ |
| Derived state | useMemo(() => x * 2, [x]) | computed(() => x.value * 2) |
| Side effect | useEffect(() => {}, [dep]) | watch(dep, callback) |
| Mount effect | useEffect(() => {}, []) | onMounted(() => {}) |
| Cleanup | useEffect(() => { return cleanup }) | onUnmounted(cleanup) |
| Auto-effect | N/A | watchEffect(() => {}) |
| Props | function Comp({ name }) | defineProps({ name: String }) |
| Events | props.onClick() | emit('click') |
| Children | props.children | <slot /> |
| Render props | props.render(data) | <slot :data="data" /> (scoped slot) |
| Context | createContext + useContext | provide + inject |
| Ref to DOM | useRef(null) | const el = ref(null) + ref="el" |
| Conditional | {show && <Comp/>} | <Comp v-if="show" /> |
| List render | arr.map(x => <li key={x.id}>) | <li v-for="x in arr" :key="x.id"> |
| Two-way bind | value={x} onChange={e => setX(e)} | v-model="x" |
| CSS scope | CSS Modules / styled-components | <style scoped> |
| Custom hook | useXxx() | useXxx() (composable) |
| State mgmt | Redux / Zustand | Pinia |
| Router | react-router | vue-router |
| Lazy load | React.lazy(() => import(...)) | defineAsyncComponent(() => import(...)) |
| Portal | createPortal(el, target) | <Teleport to="target"> |
| Error boundary | <ErrorBoundary> | onErrorCaptured() |
| Suspense | <Suspense fallback={...}> | <Suspense> + #fallback slot |
| Fragment | <>...</> | Multiple root elements (built-in) |
| Dynamic comp | Conditional rendering | <component :is="comp" /> |
Common Patterns Side-by-Side (Yaygın Kalıplar Karşılaştırmalı)
Counter Example (Sayaç Örneği):
<!-- Vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>// 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 -->
<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>// 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ı)
# 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 lintPackage Installation (Paket Kurulumu)
# 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 LibraryVue CLI Commands (Vue CLI Komutları)
# 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 componentsUseful Code Snippets (Faydalı Kod Parçaları)
<!-- 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>/* Transition CSS classes */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}<!-- 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)
<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
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)
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run devNuxt 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 componentFile-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/settingsuseFetch ve useAsyncData
<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
// 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) |
|---|---|---|
| Framework | Vue 3 | React |
| Routing | File-based (otomatik) | File-based (otomatik) |
| SSR | Varsayılan (default) | Varsayılan (default) |
| SSG | nuxt generate | next export / static |
| API Routes | server/api/ | app/api/ |
| Veri çekme (data fetching) | useFetch, useAsyncData | fetch (Server Components) |
| Meta framework | Nitro (sunucu motoru) | Vercel altyapısı |
| Öğrenme eğrisi (learning curve) | Vue biliyorsan kolay | React 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:
<!-- 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:
npm install dompurifyimport DOMPurify from 'dompurify'
const safeHtml = computed(() => DOMPurify.sanitize(rawHtml.value))Auth Guard ile Rota Koruma (Route Protection with Auth Guard)
// 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)
// Kullanıcı girdilerini her zaman doğrulayın (always validate user inputs)
function sanitizeInput(input) {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// 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)
<!-- 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)
npm install -D vitest @vue/test-utils jsdom// vite.config.js
export default defineConfig({
test: {
environment: 'jsdom',
globals: true
}
})Temel Bileşen Testi (Basic Component Test)
// 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)
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)
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çinreactivekullanın:refher yerde çalışır ve daha tahmin edilebilir computedkullanın, method yerine: computed değerleri önbelleğeler, method her renderda yeniden çalışırtoRefsile reactive destructure edin:const { name } = toRefs(state)reaktiviteyi korur, direkt destructure reaktiviteyi kırardefinePropsvedefineEmitskullanın:<script setup>içerisinde tip güvenli props ve events tanımlayın- Store'larda
storeToRefskullanın: Pinia store'dan state/getters çıkarırkenstoreToRefskullanmazsanız reaktivite kaybolur - Büyük bileşenleri composable'lara bölün: 200+ satırlık bileşenler yerine mantığı
usefonksiyonlarına çıkartın watchEffectbasit izleme,watchdetaylı 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.)
İlgili Rehberler (Related Guides)
Frontend
- Frontend Genel Bakış
- JavaScript & TypeScript
- TypeScript Rehberi
- React Rehberi
- Next.js Rehberi
- CSS & Tailwind
- Web Performance
- Three.js Rehberi