Nuxt.js'de Modern State Management: Composables, Pinia ve SSR

Yunus Emre Güzel
15 Ocak 202520 dkVue.js
Nuxt.js'de Modern State Management: Composables, Pinia ve SSR

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

  1. Composables ile State Management
  2. Pinia Store Yapılandırması
  3. SSR Uyumlu State Yönetimi
  4. Hydration ve State Senkronizasyonu
  5. 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ı

  1. Type Safety: TypeScript ile tam uyumluluk
  2. Code Splitting: Otomatik kod bölümleme
  3. Tree Shaking: Kullanılmayan kodların elenmesi
  4. 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.

Kaynaklar