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.