Next.js'te State Yönetimi: Zustand mı, Redux Toolkit mı?

Zustand ve Redux Toolkit'i Next.js projelerinde karşılaştırıyoruz. SSR uyumluluğu, App Router desteği, boilerplate maliyeti ve performans açısından hangisi sizin için doğru seçim? Kod örnekleriyle tam rehber.

Yazılım & Teknoloji

1 ay önce

16 okuma

9 dk okuma

Next.js projesi açıp global state yönetimi seçme aşamasına geldiğinizde, ekip içinde kaçınılmaz bir tartışma başlar. Redux mu kullansak? Ama çok fazla dosya açıyor. Zustand? Ama büyük projede yeterli mi? Context API? Ama performans sorunları...

Bu yazıda o tartışmayı bitirecek bir çerçeve sunmak istiyorum. Zustand ve Redux Toolkit'i Next.js ekosistemi özelinde (App Router, SSR, hidrasyon, middleware ve DevTools desteği açısından) karşılaştırıyoruz. Hangisini ne zaman seçmeniz gerektiğini de net bir şekilde ortaya koyacağız.

Önce Temel Farkı Anlayalım

Her iki kütüphane de React uygulamalarında global state yönetimi için kullanılır. Ancak felsefeleri farklıdır.

Zustand, minimal API yüzeyi ve sıfır şablona yakın kurulumu ile öne çıkar. Bir store oluşturmak birkaç satır sürer, öğrenme eğrisi neredeyse yoktur ve bundle boyutu son derece küçüktür (~1KB gzipped).

Redux Toolkit (RTK) ise Redux'un "pil dahil" halidir. Klasik Redux'un getirdiği boilerplate acısını büyük ölçüde gidermiş, üstüne DevTools entegrasyonu, RTK Query ile veri fetching ve normalize edilmiş state yapıları eklemiştir. Bundle boyutu daha büyüktür (~40KB gzipped) ama bu boyuta karşılık ciddi araç seti sunar.

Kurulum Karşılaştırması

Zustand Kurulumu

npm install zustand

Bir counter store oluşturmak:

// store/counterStore.js
import { create } from 'zustand'

export const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

Bileşende kullanım:

'use client'

import { useCounterStore } from '@/store/counterStore'

export default function Counter() {
  const { count, increment, decrement } = useCounterStore()

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

İşte bu kadar. Provider yok, boilerplate yok.

Redux Toolkit Kurulumu

npm install @reduxjs/toolkit react-redux

Aynı counter için RTK:

// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = { count: 0 }

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => { state.count += 1 },
    decrement: (state) => { state.count -= 1 },
    reset: (state) => { state.count = 0 },
    incrementByAmount: (state, action) => {
      state.count += action.payload
    },
  },
})

export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})
// app/providers.jsx (App Router için)
'use client'

import { Provider } from 'react-redux'
import { store } from '@/store'

export function ReduxProvider({ children }) {
  return <Provider store={store}>{children}</Provider>
}
// app/layout.jsx
import { ReduxProvider } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ReduxProvider>{children}</ReduxProvider>
      </body>
    </html>
  )
}

Bileşende kullanım:

'use client'

import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from '@/store/counterSlice'

export default function Counter() {
  const count = useSelector((state) => state.counter.count)
  const dispatch = useDispatch()

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

Gördüğünüz gibi aynı işlevsellik için RTK çok daha fazla dosya ve yapı gerektiriyor. Bu kötü değil, sadece farklı... ve bu fark, projenin ölçeğine göre avantaja ya da dezavantaja dönüşebilir.

Next.js App Router ile SSR Uyumluluğu

Next.js 13+ ile gelen App Router, Server Components kavramını getirdi. Bu durum, state yönetimini doğrudan etkiliyor çünkü global state kütüphaneleri doğası gereği istemci taraflıdır.

Temel Kural: State Kütüphaneleri Server Component'ta Kullanılamaz

useCounterStore() veya useSelector() çağıran her bileşen 'use client' direktifiyle işaretlenmelidir. Bu her iki kütüphane için de geçerlidir.

Ancak buradaki kritik sorun hidrasyon uyumsuzluğudur (hydration mismatch). Eğer store, sunucuda ve istemcide farklı başlangıç değerleriyle başlatılırsa React bir uyarı verir ve bazı durumlarda uygulama bozulur.

Zustand ile SSR Güvenli Store

Zustand bu sorunu çözmek için createWithEqualityFn ve özellikle bir pattern önerir:

// store/counterStore.js
import { create } from 'zustand'

// Store factory — her istek için yeni instance
const createCounterStore = (initialCount = 0) =>
  create((set) => ({
    count: initialCount,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))

export { createCounterStore }
// app/providers/CounterStoreProvider.jsx
'use client'

import { createContext, useContext, useRef } from 'react'
import { useStore } from 'zustand'
import { createCounterStore } from '@/store/counterStore'

const CounterStoreContext = createContext(null)

export function CounterStoreProvider({ children, initialCount }) {
  const storeRef = useRef()
  if (!storeRef.current) {
    storeRef.current = createCounterStore(initialCount)
  }

  return (
    <CounterStoreContext.Provider value={storeRef.current}>
      {children}
    </CounterStoreContext.Provider>
  )
}

export function useCounterStore(selector) {
  const store = useContext(CounterStoreContext)
  if (!store) throw new Error('CounterStoreProvider bulunamadı')
  return useStore(store, selector)
}

Bu pattern biraz karmaşık görünse de Zustand'ın resmi Next.js dokümantasyonunda önerilen yaklaşımdır. Amaç, her kullanıcı isteğinin izole bir store instance'ı kullanmasını sağlamaktır.

RTK ile SSR

RTK'nın Next.js ile kullanımında da benzer bir store factory yaklaşımı önerilir:

// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const makeStore = (preloadedState = {}) =>
  configureStore({
    reducer: { counter: counterReducer },
    preloadedState,
  })
// app/providers/StoreProvider.jsx
'use client'

import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '@/store'

export function StoreProvider({ children, preloadedState = {} }) {
  const storeRef = useRef()
  if (!storeRef.current) {
    storeRef.current = makeStore(preloadedState)
  }

  return <Provider store={storeRef.current}>{children}</Provider>
}

Sunucu bileşeninden başlangıç verisini provider'a iletmek:

// app/page.jsx (Server Component)
import { StoreProvider } from './providers/StoreProvider'
import Counter from './Counter'

async function getInitialData() {
  // API'den veri çek
  return { counter: { count: 42 } }
}

export default async function Page() {
  const preloadedState = await getInitialData()

  return (
    <StoreProvider preloadedState={preloadedState}>
      <Counter />
    </StoreProvider>
  )
}

Zustand persist Middleware: Kalıcı State

Zustand'ın en sevilen özelliklerinden biri persist middleware'idir. Kullanıcı tercihlerini, tema seçimini veya alışveriş sepetini localStorage'da saklamak çok kolaydır:

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

export const useThemeStore = create()(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () =>
        set((state) => ({
          theme: state.theme === 'light' ? 'dark' : 'light',
        })),
    }),
    {
      name: 'theme-storage',
      storage: createJSONStorage(() => localStorage),
    }
  )
)

RTK'da aynı işlev için redux-persist adlı ayrı bir paket gerekir ve kurulumu çok daha karmaşıktır.

RTK Query: Veri Fetching'i Yeniden Tanımlamak

RTK'nın en güçlü kozlarından biri RTK Query'dir. Sunucu ile istemci state'ini birbirinden net şekilde ayırır ve loading, error, caching, refetching gibi durumları otomatik olarak yönetir:

// store/api/productsApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const productsApi = createApi({
  reducerPath: 'productsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Product'],
  endpoints: (builder) => ({
    getProducts: builder.query({
      query: () => '/products',
      providesTags: ['Product'],
    }),
    createProduct: builder.mutation({
      query: (body) => ({ url: '/products', method: 'POST', body }),
      invalidatesTags: ['Product'],
    }),
  }),
})

export const { useGetProductsQuery, useCreateProductMutation } = productsApi
'use client'

import { useGetProductsQuery } from '@/store/api/productsApi'

export default function ProductList() {
  const { data: products, isLoading, error } = useGetProductsQuery()

  if (isLoading) return <p>Yükleniyor...</p>
  if (error) return <p>Hata oluştu</p>

  return (
    <ul>
      {products?.map((p) => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}

Bu seviyede otomasyon Zustand'da yoktur. Zustand ile veri fetching için genellikle SWR veya TanStack Query kombinasyonu tercih edilir.

DevTools Desteği

Redux DevTools Extension, state geçmişini zaman tüneli gibi izlemenizi, her action'ı kaydetmenizi ve state'i herhangi bir noktaya geri almanızı sağlar. RTK, bu araçla kutudan çıktığı gibi çalışır.

Zustand da Redux DevTools'u destekler, ancak middleware olarak manuel eklemek gerekir:

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

export const useCounterStore = create()(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
    }),
    { name: 'CounterStore' }
  )
)

Action isimlerini manuel olarak tanımlamazsanız DevTools'da tüm action'lar "anonymous" olarak görünür. Bu, RTK'ya kıyasla debug deneyimini zayıflatır.

Hangi Durumda Hangisi?

Projenize en uygun seçimi yapmak için birkaç belirleyici soruya bakın:

Zustand'ı seçin eğer:

  • Projeniz küçük-orta ölçekliyse,
  • Ekip Redux ekosistemine aşina değilse,
  • Hızlı prototipleme veya MVP aşamasındaysanız,
  • LocalStorage persist ihtiyacınız varsa veya bundle boyutunu minimize etmek önceliğinizse.

Redux Toolkit'i seçin eğer:

  • Büyük bir ekiple kurumsal ölçekli proje geliştiriyorsanız,
  • Karmaşık asenkron iş akışları ve middleware zinciri gerekiyorsa,
  • RTK Query ile veri fetching ve cache yönetimini tek çatı altında toplamak istiyorsanız veya Redux DevTools ile kapsamlı debug altyapısı kritikse.

Zustand + TanStack Query: Güçlü Alternatif Kombinasyon

Next.js projelerinde giderek yaygınlaşan bir yaklaşım, sunucu state'i için TanStack Query (React Query), istemci UI state'i için Zustand kullanmaktır:

// Sunucu state: TanStack Query
import { useQuery } from '@tanstack/react-query'

export function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(r => r.json()),
  })
}

// İstemci UI state: Zustand
import { create } from 'zustand'

export const useUIStore = create((set) => ({
  selectedProductId: null,
  isFilterOpen: false,
  setSelectedProduct: (id) => set({ selectedProductId: id }),
  toggleFilter: () => set((state) => ({ isFilterOpen: !state.isFilterOpen })),
}))

Bu ayrım çok temizdir: ne zaman API'den veri çektiğinizi ve ne zaman UI state'ini değiştirdiğinizi net bir şekilde kodunuza yansıtır. RTK Query + RTK'nın sunduğu tek çatı alternatifine kıyasla daha modüler bir yapı sağlar.

Performans: Gereksiz Re-render'dan Kaçınma

Zustand'da Selector Kullanımı

Zustand'da bileşenler yalnızca seçilen state dilimi değiştiğinde yeniden render edilir:

// Tüm state değiştiğinde render olur — kötü pratik
const state = useCounterStore()

// Yalnızca count değiştiğinde render olur — doğru kullanım
const count = useCounterStore((state) => state.count)

RTK'da Memoized Selector

RTK'da createSelector ile memoize edilmiş selector'lar oluşturabilirsiniz:

import { createSelector } from '@reduxjs/toolkit'

const selectCounter = (state) => state.counter
export const selectDoubleCount = createSelector(
  selectCounter,
  (counter) => counter.count * 2
)

Her iki kütüphane de doğru kullanıldığında gereksiz re-render sorununu efektif biçimde çözer.

Özet

Zustand ve Redux Toolkit, birbirinin rakibi değil, farklı ihtiyaçlara yanıt veren iki olgun araçtır. Next.js App Router ekosisteminde ikisi de çalışır; ancak SSR uyumluluğu için her ikisinde de store factory pattern'i ve dikkatli hidrasyon yönetimi gereklidir.

Eğer hızlı ve minimal bir başlangıç arıyorsanız Zustand'ın çekiciliği yadsınamaz. Eğer büyük bir organizasyonda standartlaşmış, araç destekli ve ölçeklenebilir bir mimari kuruyorsanız Redux Toolkit'in getirdiği yapı size uzun vadede geri ödenir.

Next.js projelerinde artık çok yaygın görülen Zustand + TanStack Query kombinasyonu ise ikisinin en iyi yanlarını bir araya getiren pragmatik bir üçüncü yol olarak değerlendirilebilir.

zustand vs redux toolkitnext.js state yönetimizustand nextjsredux toolkit nextjsapp router state managementzustand ssrredux toolkit ssrreact state yönetimi karşılaştırmanextjs global statezustand persistredux toolkit slice

Geri

Paylaş

Ayarlar