Nuxt.js'de Modern State Management: Composables, Pinia ve SSR
Nuxt.js 3 ile birlikte state management yaklaşımları önemli ölçüde evrildi. Composition API ve yeni araçların gelişiyle birlikte, state yönetimi daha modüler ve type-safe hale geldi. Bu yazıda, Nuxt.js uygulamalarında modern state yönetimi tekniklerini, Composables API kullanımını ve Pinia entegrasyonunu detaylıca inceleyeceğiz.
Modern web uygulamalarında state yönetimi, uygulamanın kalbi niteliğindedir. Özellikle Nuxt.js gibi full-stack framework'lerde, client ve server arasındaki state senkronizasyonu kritik önem taşır. Bu makalede, Nuxt.js 3'ün sunduğu modern araçları kullanarak nasıl güvenli, ölçeklenebilir ve performanslı bir state yönetimi yapısı kurabileceğimizi öğreneceğiz.
İçerik
- Composables ile State Management
- Pinia Store Yapılandırması
- SSR Uyumlu State Yönetimi
- Hydration ve State Senkronizasyonu
- Best Practices ve Performans Optimizasyonları
Her bir başlık, modern Nuxt.js uygulamalarında karşılaşacağınız gerçek dünya senaryolarını ele alacak ve pratik çözümler sunacak. Örneklerimizde TypeScript kullanarak tip güvenliğini en üst düzeyde tutacak ve SSR uyumlu kod yazmanın inceliklerini paylaşacağız.
1. Composables ile State Management
Nuxt.js 3'te Composables API, state yönetimi için güçlü ve esnek bir çözüm sunuyor. Vue 3'ün Composition API'si üzerine inşa edilen bu yaklaşım, state mantığını yeniden kullanılabilir ve test edilebilir parçalara ayırmamıza olanak tanır. Composables, basit bir sayaç uygulamasından karmaşık form yönetimine kadar her seviyede state yönetimi ihtiyacını karşılayabilir.
Composables yaklaşımı, geleneksel Vuex veya basit reactive değişkenler yerine, daha organize ve modüler bir yapı sunar. Bu yapı sayesinde state logic'inizi kolayca test edebilir, farklı projelerde yeniden kullanabilir ve tip güvenliğini maksimum seviyede tutabilirsiniz. İşte temel yaklaşımlar:
// composables/useCounter.ts
export const useCounter = () => {
// State tanımlaması
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
// Actions
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = 0
// State persistence
const persistedCount = useCookie('count')
watch(count, (newValue) => {
persistedCount.value = newValue.toString()
})
// SSR için initial state
if (process.server) {
count.value = Number(persistedCount.value) || 0
}
return {
count: readonly(count),
doubleCount,
increment,
decrement,
reset
}
}
Composables'ın Avantajları
- Type Safety: TypeScript ile tam uyumluluk
- Code Splitting: Otomatik kod bölümleme
- Tree Shaking: Kullanılmayan kodların elenmesi
- SSR Uyumluluğu: Server-side rendering desteği
2. Pinia Store Yapılandırması
Büyük ve karmaşık uygulamalarda merkezi bir state yönetimi vazgeçilmezdir. Pinia, Vue.js ekosisteminin yeni nesil state management çözümü olarak, TypeScript ile tam uyumlu, devtools desteği güçlü ve modüler bir yapı sunar. Vuex'in yerini alan Pinia, daha hafif, daha hızlı ve daha modern bir yaklaşım benimser.
Pinia'nın en güçlü yanlarından biri, store'ları modüler bir şekilde organize etme yeteneğidir. Her bir store, kendi state, getters ve actions'larıyla bağımsız bir modül olarak çalışır. Bu modülerlik, büyük uygulamaları daha yönetilebilir parçalara bölmemize ve kod organizasyonunu iyileştirmemize olanak tanır. İşte type-safe ve modüler bir Pinia store örneği:
// stores/user.ts
import { defineStore } from 'pinia'
interface User {
id: string
name: string
email: string
preferences: {
theme: 'light' | 'dark'
notifications: boolean
}
}
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
isLoading: false,
error: null as string | null
}),
getters: {
isAuthenticated: (state) => !!state.user,
userTheme: (state) => state.user?.preferences.theme ?? 'light'
},
actions: {
async fetchUser() {
this.isLoading = true
try {
const { data } = await useFetch('/api/user')
this.user = data.value
this.error = null
} catch (err) {
this.error = err.message
} finally {
this.isLoading = false
}
},
async updatePreferences(preferences: Partial<User['preferences']>) {
if (!this.user) return
const updatedPreferences = {
...this.user.preferences,
...preferences
}
await $fetch('/api/user/preferences', {
method: 'PUT',
body: updatedPreferences
})
this.user.preferences = updatedPreferences
}
}
})
Pinia'nın SSR Entegrasyonu
// plugins/pinia.ts
import { defineNuxtPlugin } from '#app'
import { createPinia } from 'pinia'
export default defineNuxtPlugin(({ vueApp }) => {
const pinia = createPinia()
vueApp.use(pinia)
// SSR state hydration
if (process.client) {
const initialState = window.__NUXT__?.state
if (initialState) {
pinia.state.value = initialState
}
}
return {
provide: {
pinia
}
}
})
3. SSR Uyumlu State Yönetimi
Server-side rendering ile state yönetimi özel dikkat gerektirir. İşte önemli noktalar:
// composables/useAsyncData.ts
export const useAsyncData = <T>(key: string, fetcher: () => Promise<T>) => {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(true)
// SSR için veri fetch
if (process.server) {
const nuxtApp = useNuxtApp()
nuxtApp.payload[`async_${key}`] = fetcher()
.then(result => {
data.value = result
return result
})
.catch(err => {
error.value = err
return null
})
.finally(() => {
isLoading.value = false
})
}
// Client-side hydration
if (process.client) {
const nuxtApp = useNuxtApp()
const savedData = nuxtApp.payload[`async_${key}`]
if (savedData) {
data.value = savedData
isLoading.value = false
}
}
return {
data: readonly(data),
error: readonly(error),
isLoading: readonly(isLoading)
}
}
4. Hydration ve State Senkronizasyonu
State'in server ve client arasında doğru senkronize edilmesi kritiktir:
// composables/useHybridState.ts
export const useHybridState = <T>(
key: string,
initialValue: T,
options: {
persist?: boolean
ssr?: boolean
} = {}
) => {
const state = ref<T>(initialValue)
// Cookie persistence
if (options.persist) {
const cookie = useCookie<T>(key, {
maxAge: 60 * 60 * 24 * 7, // 1 hafta
watch: true
})
watch(state, (newValue) => {
cookie.value = newValue
})
// Cookie'den initial değeri al
if (cookie.value) {
state.value = cookie.value
}
}
// SSR state handling
if (options.ssr && process.server) {
const nuxtApp = useNuxtApp()
nuxtApp.payload[`state_${key}`] = state.value
}
if (process.client) {
const nuxtApp = useNuxtApp()
const savedState = nuxtApp.payload[`state_${key}`]
if (savedState) {
state.value = savedState
}
}
return {
state: readonly(state),
setState: (value: T) => {
state.value = value
}
}
}
5. Best Practices ve Performans Optimizasyonları
5.1 State Modülarizasyonu
// stores/modules/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
discounts: [] as Discount[]
}),
getters: {
totalItems: (state) => state.items.length,
subtotal: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
totalDiscount: (state) => state.discounts.reduce((sum, discount) => sum + discount.amount, 0),
total: (state) => {
const subtotal = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const discount = state.discounts.reduce((sum, discount) => sum + discount.amount, 0)
return subtotal - discount
}
},
actions: {
async addItem(item: CartItem) {
// Optimistic update
this.items.push(item)
try {
await $fetch('/api/cart/items', {
method: 'POST',
body: item
})
} catch (error) {
// Rollback on error
this.items = this.items.filter(i => i.id !== item.id)
throw error
}
}
}
})
5.2 Performans Optimizasyonları
// composables/useOptimizedState.ts
export const useOptimizedState = <T>(options: {
key: string
initialValue: T
debounceMs?: number
cacheStrategy?: 'memory' | 'localStorage' | 'none'
}) => {
const state = ref<T>(options.initialValue)
const debouncedState = useDebounce(state, options.debounceMs || 300)
// Cache stratejisi
if (options.cacheStrategy === 'localStorage') {
const cached = localStorage.getItem(options.key)
if (cached) {
try {
state.value = JSON.parse(cached)
} catch (e) {
console.error('Cache parsing error:', e)
}
}
watch(debouncedState, (newValue) => {
localStorage.setItem(options.key, JSON.stringify(newValue))
})
}
// Memory cache için WeakMap kullanımı
if (options.cacheStrategy === 'memory') {
const cache = new WeakMap()
watch(debouncedState, (newValue) => {
if (typeof newValue === 'object' && newValue !== null) {
cache.set(newValue, true)
}
})
}
return {
state: readonly(state),
setState: (value: T) => {
state.value = value
}
}
}
Sonuç
Modern Nuxt.js uygulamalarında state management, Composables API ve Pinia'nın güçlü özelliklerini birleştirerek daha modüler ve type-safe bir yapı sunuyor. SSR uyumluluğu ve performans optimizasyonları ile birlikte, büyük ölçekli uygulamalarda bile etkili state yönetimi mümkün hale geliyor.