İçeriğe geç
react

Context API Performans Sorunları ve Çözümleri

Tarık Tunç
Context API Performans Sorunları ve Çözümleri

Context API Performans Sorunları ve Çözümleri

React Context API, component ağacında veri paylaşımı için güçlü bir araçtır. Ancak yanlış kullanıldığında ciddi performans sorunlarına yol açabilir. Bu rehberde, Context API'nin neden performans sorunu yarattığını, bu sorunları nasıl tespit edeceğinizi ve pratik çözüm stratejilerini detaylı örneklerle ele alacağız.

Sorunun Kaynağı: Tüm Consumer'lar Yeniden Render Olur

Context API'nin en büyük performans sorunu, context value değiştiğinde o context'i consume eden tüm componentlerin — değişen kısımla ilgilenmeseler bile — yeniden render olmasıdır. Bu, özellikle büyük uygulamalarda zincirleme render'lara ve performans düşüşüne neden olur.

import React, { createContext, useContext, useState, ReactNode } from 'react';

// ❌ KÖTÜ: Tek bir büyük context
interface AppState {
  user: { name: string; email: string } | null;
  theme: 'light' | 'dark';
  language: string;
  notifications: Notification[];
  cart: CartItem[];
  sidebarOpen: boolean;
}

const AppContext = createContext<{
  state: AppState;
  setState: React.Dispatch<React.SetStateAction<AppState>>;
} | null>(null);

function AppProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<AppState>({
    user: null,
    theme: 'light',
    language: 'tr',
    notifications: [],
    cart: [],
    sidebarOpen: false,
  });

  // Her state değişikliğinde TÜM consumer'lar render olur!
  // Sidebar açıldığında bile cart componentleri render olur.
  return (
    <AppContext.Provider value={{ state, setState }}>
      {children}
    </AppContext.Provider>
  );
}
Uyarı: Yukarıdaki pattern, React uygulamalarındaki en yaygın performans hatalarından biridir. sidebarOpen değiştiğinde, sepet (cart) ve bildirim (notifications) componentleri de gereksiz yere render olacaktır.

Çözüm 1: Context'i Parçala

En etkili çözüm, büyük context'i mantıksal parçalara bölmektir. Her context sadece ilgili verileri içerir ve sadece ilgili consumer'ları etkiler.

// ✅ İYİ: Ayrı context'ler
const ThemeContext = createContext<{
  theme: 'light' | 'dark';
  toggleTheme: () => void;
} | null>(null);

const AuthContext = createContext<{
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
} | null>(null);

const CartContext = createContext<{
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: number;
} | null>(null);

// Her provider bağımsız
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggleTheme = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));

  // useMemo ile value referansını sabitliyoruz
  const value = React.useMemo(() => ({ theme, toggleTheme }), [theme]);

  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = React.useCallback((item: CartItem) => {
    setItems((prev) => [...prev, item]);
  }, []);

  const removeItem = React.useCallback((id: string) => {
    setItems((prev) => prev.filter((item) => item.id !== id));
  }, []);

  const total = React.useMemo(
    () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );

  const value = React.useMemo(
    () => ({ items, addItem, removeItem, total }),
    [items, addItem, removeItem, total]
  );

  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

// App'te nested provider'lar
function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <CartProvider>
          <MainLayout />
        </CartProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

Çözüm 2: State ve Dispatch'i Ayırma

State okuyan ve state güncelleyen componentlerin ayrı context'ler kullanması, gereksiz render'ları önemli ölçüde azaltır. Bir buton sadece dispatch fonksiyonuna ihtiyaç duyuyorsa, state değişikliklerinden etkilenmez.

import React, { createContext, useContext, useReducer, ReactNode, Dispatch } from 'react';

// State tipi
interface CounterState {
  count: number;
  step: number;
}

// Action tipleri
type CounterAction =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET_STEP'; payload: number }
  | { type: 'RESET' };

// Reducer
function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + state.step };
    case 'DECREMENT':
      return { ...state, count: state.count - state.step };
    case 'SET_STEP':
      return { ...state, step: action.payload };
    case 'RESET':
      return { count: 0, step: 1 };
    default:
      return state;
  }
}

// Ayrı context'ler — state ve dispatch
const CounterStateContext = createContext<CounterState | null>(null);
const CounterDispatchContext = createContext<Dispatch<CounterAction> | null>(null);

function CounterProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });

  return (
    <CounterStateContext.Provider value={state}>
      <CounterDispatchContext.Provider value={dispatch}>
        {children}
      </CounterDispatchContext.Provider>
    </CounterStateContext.Provider>
  );
}

// Custom hook'lar
function useCounterState() {
  const context = useContext(CounterStateContext);
  if (!context) throw new Error('useCounterState must be used within CounterProvider');
  return context;
}

function useCounterDispatch() {
  const context = useContext(CounterDispatchContext);
  if (!context) throw new Error('useCounterDispatch must be used within CounterProvider');
  return context;
}

// Sadece state okuyan component — dispatch değişse render olmaz
function CounterDisplay() {
  const { count } = useCounterState();
  console.log('CounterDisplay rendered');
  return <h2>Sayaç: {count}</h2>;
}

// Sadece dispatch kullanan component — state değişse render olmaz
function CounterButtons() {
  const dispatch = useCounterDispatch();
  console.log('CounterButtons rendered');
  return (
    <div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Sıfırla</button>
    </div>
  );
}
İpucu: useReducer + ayrı context pattern'i, Redux benzeri bir yapı sunar ve büyük uygulamalarda Context API'nin performans sorunlarını büyük ölçüde çözer. Ancak çok karmaşık state yapılarında Zustand veya Jotai'yi değerlendirin.

Çözüm 3: useMemo ile Value Memoizasyonu

// ❌ KÖTÜ: Her render'da yeni object referansı
function BadProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // Her render'da yeni object — tüm consumer'lar render olur!
  return (
    <MyContext.Provider value={{ count, name, setCount, setName }}>
      {children}
    </MyContext.Provider>
  );
}

// ✅ İYİ: useMemo ile memoize edilmiş value
function GoodProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const value = React.useMemo(
    () => ({ count, name, setCount, setName }),
    [count, name] // Sadece count veya name değiştiğinde yeni object oluşur
  );

  return (
    <MyContext.Provider value={value}>
      {children}
    </MyContext.Provider>
  );
}
Bilgi: React RFC olarak sunulan useContextSelector hook'u, context'ten sadece belirli bir değeri seçmenizi sağlayacaktır. Bu hook resmi React'e eklenene kadar, use-context-selector kütüphanesini kullanabilirsiniz.

Ne Zaman Context API Yerine Alternatif Kullanmalı?

  • Sık değişen veriler (her 100ms'de): Zustand veya Jotai tercih edin
  • Karmaşık state ağacı (5+ parça): Redux Toolkit veya Zustand tercih edin
  • Server state (API verileri): TanStack Query kullanın, Context ile yönetmeyin
  • Tema, dil, auth gibi nadir değişen veriler: Context API ideal

Sonuç

Context API performans sorunları, doğru mimari kararlarla büyük ölçüde çözülebilir. Context'leri parçalayın, state ve dispatch'i ayırın, useMemo ile value referanslarını sabitleyin ve sık değişen veriler için harici state kütüphanelerini değerlendirin. Unutmayın: Context API her sorun için doğru araç değildir — doğru aracı doğru iş için kullanmak, en iyi performans stratejisidir.

Kaynaklar

İlgili Yazılar