İçeriğe geç
react

useReducer Hook'u: Karmaşık State Mantığını Yönetmek

Tarık Tunç
useReducer Hook'u: Karmaşık State Mantığını Yönetmek

useReducer Hook'u: Karmaşık State Mantığını Yönetmek

React'te state yönetimi genellikle useState ile başlar, ancak bileşenin state mantığı karmaşıklaştıkça useReducer daha uygun bir seçenek haline gelir. Redux'tan ilham alan bu hook, state geçişlerini öngörülebilir ve test edilebilir kılar. Bu yazıda useReducer'ın çalışma mekanizmasını, gerçek dünya örneklerini ve ileri seviye pattern'leri inceleyeceğiz.

useReducer'ın Temeli

useReducer, bir reducer fonksiyonu ve başlangıç state'i alır. Dispatch fonksiyonu ile action'lar göndererek state'i güncellersiniz:

import { useReducer } from 'react';

// Reducer fonksiyonu - pure function olmalıdır
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'INCREMENT_BY':
      return { ...state, count: state.count + action.payload };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      throw new Error(`Bilinmeyen action: ${action.type}`);
  }
}

// Başlangıç state'i
const initialState = { count: 0 };

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <p>Sayaç: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>+5</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Sıfırla</button>
    </div>
  );
}
Reducer fonksiyonu saf (pure) olmalıdır: aynı state ve action ile her zaman aynı sonucu üretmelidir. API çağrıları, DOM manipülasyonları gibi yan etkiler reducer içinde yapılmamalıdır.

Gerçek Dünya Örneği: Alışveriş Sepeti

// Reducer tanımı
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id
      );

      if (existingIndex >= 0) {
        // Ürün zaten sepette - miktarı artır
        const updatedItems = state.items.map((item, index) =>
          index === existingIndex
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
        return { ...state, items: updatedItems };
      }

      // Yeni ürün ekle
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(0, action.payload.quantity) }
            : item
        ).filter(item => item.quantity > 0)
      };

    case 'APPLY_COUPON':
      return { ...state, coupon: action.payload, discount: action.payload.discount };

    case 'CLEAR_CART':
      return { ...state, items: [], coupon: null, discount: 0 };

    default:
      throw new Error(`Bilinmeyen action: ${action.type}`);
  }
}

const initialCartState = {
  items: [],
  coupon: null,
  discount: 0
};

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, initialCartState);

  const subtotal = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  );
  const discountAmount = subtotal * (cart.discount / 100);
  const total = subtotal - discountAmount;

  return (
    <div className="shopping-cart">
      <h2>Sepetim ({cart.items.length} ürün)</h2>

      {cart.items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name} - {item.price} TL</span>
          <div className="quantity-controls">
            <button onClick={() => dispatch({
              type: 'UPDATE_QUANTITY',
              payload: { id: item.id, quantity: item.quantity - 1 }
            })}>-</button>
            <span>{item.quantity}</span>
            <button onClick={() => dispatch({
              type: 'UPDATE_QUANTITY',
              payload: { id: item.id, quantity: item.quantity + 1 }
            })}>+</button>
          </div>
          <button onClick={() => dispatch({
            type: 'REMOVE_ITEM', payload: item.id
          })}>Kaldır</button>
        </div>
      ))}

      <div className="cart-summary">
        <p>Ara Toplam: {subtotal.toFixed(2)} TL</p>
        {cart.coupon && <p>İndirim: -{discountAmount.toFixed(2)} TL</p>}
        <p><strong>Toplam: {total.toFixed(2)} TL</strong></p>
      </div>

      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
        Sepeti Temizle
      </button>
    </div>
  );
}

Form Yönetimi ile useReducer

function formReducer(state, action) {
  switch (action.type) {
    case 'FIELD_CHANGE':
      return {
        ...state,
        values: { ...state.values, [action.field]: action.value },
        errors: { ...state.errors, [action.field]: '' },
        touched: { ...state.touched, [action.field]: true }
      };

    case 'VALIDATE':
      return { ...state, errors: action.errors };

    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, submitError: null };

    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, isSubmitted: true };

    case 'SUBMIT_ERROR':
      return { ...state, isSubmitting: false, submitError: action.error };

    case 'RESET':
      return initialFormState;

    default:
      return state;
  }
}

const initialFormState = {
  values: { name: '', email: '', message: '' },
  errors: {},
  touched: {},
  isSubmitting: false,
  isSubmitted: false,
  submitError: null
};

function ContactForm() {
  const [form, dispatch] = useReducer(formReducer, initialFormState);

  const handleChange = (e) => {
    dispatch({
      type: 'FIELD_CHANGE',
      field: e.target.name,
      value: e.target.value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_START' });

    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form.values)
      });
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR', error: error.message });
    }
  };

  if (form.isSubmitted) {
    return <p>Mesajınız gönderildi!</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.values.name} onChange={handleChange} />
      <input name="email" value={form.values.email} onChange={handleChange} />
      <textarea name="message" value={form.values.message} onChange={handleChange} />
      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Gönderiliyor...' : 'Gönder'}
      </button>
      {form.submitError && <p className="error">{form.submitError}</p>}
    </form>
  );
}

Bilgi: useReducer ile Context API birleştirilerek mini bir state management sistemi oluşturulabilir. Bu yaklaşım, küçük ve orta ölçekli uygulamalarda Redux'a ihtiyaç duymadan global state yönetimi sağlar.

useReducer + Context Birlikte Kullanımı

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, initialCartState);

  return (
    <CartContext.Provider value={cart}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartContext.Provider>
  );
}

function useCart() {
  return useContext(CartContext);
}

function useCartDispatch() {
  return useContext(CartDispatchContext);
}

// Kullanım - herhangi bir bileşende
function AddToCartButton({ product }) {
  const dispatch = useCartDispatch();

  return (
    <button onClick={() => dispatch({ type: 'ADD_ITEM', payload: product })}>
      Sepete Ekle
    </button>
  );
}

Uyarı: Reducer fonksiyonunda default case'de mevcut state'i döndürmek yerine hata fırlatmak, geliştirme sırasında hatalı action tiplerini erken yakalamanızı sağlar. Production'da ise loglama yaparak state'i korumak tercih edilebilir.

Sonuç

useReducer, karmaşık state mantığını düzenli, öngörülebilir ve test edilebilir hale getirir. Özellikle birbiriyle ilişkili birden fazla state değeri, karmaşık güncelleme mantıkları ve state geçişlerinin belgelenmesi gereken durumlarda useState'e güçlü bir alternatiftir. Context API ile birleştirerek uygulamanız için ölçeklenebilir bir state yönetim çözümü oluşturabilirsiniz.

Kaynaklar

İlgili Yazılar