İçeriğe geç
react

useState Hook'u: İleri Seviye Teknikler ve Pattern'ler

Tarık Tunç
useState Hook'u: İleri Seviye Teknikler ve Pattern'ler

useState Hook'u: İleri Seviye Teknikler ve Pattern'ler

React'in en temel hook'u olan useState, basit görünümünün ardında birçok ileri seviye kullanım tekniği barındırır. Bu yazıda useState'in derinlemesine çalışma mekanizmasını, ileri seviye pattern'leri ve performans optimizasyonlarını inceleyeceğiz. Temel useState bilgisine sahip olduğunuzu varsayarak, sizi bir üst seviyeye taşıyacak tekniklere odaklanacağız.

Fonksiyonel Güncelleme Pattern'i

State güncellemelerinin önceki state'e bağlı olduğu her durumda fonksiyonel güncelleme kullanılmalıdır. Bu pattern, closure sorunlarını önler ve batch güncellemelerde doğru sonuçlar üretir:

import { useState } from 'react';

function AsyncCounter() {
  const [count, setCount] = useState(0);

  // Closure sorunu yaşayan versiyon
  const incrementWithDelay = () => {
    setTimeout(() => {
      // count, closure nedeniyle eski değeri tutar
      setCount(count + 1); // Her zaman aynı değeri kullanır
    }, 1000);
  };

  // Fonksiyonel güncelleme ile doğru versiyon
  const safeIncrementWithDelay = () => {
    setTimeout(() => {
      setCount(prev => prev + 1); // Her zaman güncel değeri kullanır
    }, 1000);
  };

  // Toplu güncelleme örneği
  const incrementByThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    // Sonuç: count + 3
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={safeIncrementWithDelay}>+1 (1sn sonra)</button>
      <button onClick={incrementByThree}>+3</button>
    </div>
  );
}
Fonksiyonel güncelleme pattern'i sadece önceki state'e bağlı güncellemelerde gereklidir. setName('Tarik') gibi bağımsız güncellemelerde normal kullanım yeterlidir.

Lazy Initialization ile Performans

useState'e başlangıç değeri olarak bir fonksiyon geçirmek, o fonksiyonun yalnızca ilk render'da çalışmasını sağlar:

function DataEditor() {
  // KÖTÜ: Her render'da localStorage okunur ve JSON parse edilir
  const [data, setData] = useState(
    JSON.parse(localStorage.getItem('editorData') || '{}')
  );

  // İYİ: Yalnızca ilk render'da çalışır
  const [data, setData] = useState(() => {
    try {
      const saved = localStorage.getItem('editorData');
      return saved ? JSON.parse(saved) : { content: '', settings: {} };
    } catch (error) {
      console.error('Veri okunamadı:', error);
      return { content: '', settings: {} };
    }
  });

  // Karmaşık başlangıç hesaplaması
  const [matrix, setMatrix] = useState(() => {
    return Array.from({ length: 100 }, (_, row) =>
      Array.from({ length: 100 }, (_, col) => ({
        row,
        col,
        value: 0,
        isActive: false
      }))
    );
  });

  return <div>{/* editor UI */}</div>;
}

İpucu: Lazy initialization özellikle şu durumlarda kullanılmalıdır: localStorage/sessionStorage okuma, karmaşık veri yapıları oluşturma, hesaplama maliyetli başlangıç değerleri. Basit primitive değerler için gerekli değildir.

Object ve Array State Yönetimi

Karmaşık veri yapılarıyla çalışırken immutability kurallarına dikkat etmek kritiktir:

function UserManager() {
  const [users, setUsers] = useState([
    { id: 1, name: 'Ali', skills: ['React', 'Node'], address: { city: 'İstanbul' } }
  ]);

  // Nested object güncelleme
  const updateCity = (userId, newCity) => {
    setUsers(prev => prev.map(user =>
      user.id === userId
        ? { ...user, address: { ...user.address, city: newCity } }
        : user
    ));
  };

  // Array içinde eleman ekleme (belirli pozisyona)
  const addSkillAt = (userId, skill, index) => {
    setUsers(prev => prev.map(user =>
      user.id === userId
        ? {
            ...user,
            skills: [
              ...user.skills.slice(0, index),
              skill,
              ...user.skills.slice(index)
            ]
          }
        : user
    ));
  };

  // Array içinde eleman güncelleme
  const updateSkill = (userId, oldSkill, newSkill) => {
    setUsers(prev => prev.map(user =>
      user.id === userId
        ? {
            ...user,
            skills: user.skills.map(s => s === oldSkill ? newSkill : s)
          }
        : user
    ));
  };

  // Sıralama (yeni array oluşturarak)
  const sortByName = () => {
    setUsers(prev => [...prev].sort((a, b) => a.name.localeCompare(b.name, 'tr')));
  };

  return <div>{/* UI */}</div>;
}

useState ile Immer Kullanımı

Derin iç içe geçmiş yapıları güncellemek spread operatörü ile karmaşık hale gelebilir. Immer kütüphanesi bu sorunu çözer:

import { useState, useCallback } from 'react';
import { produce } from 'immer';

function ComplexFormWithImmer() {
  const [formState, setFormState] = useState({
    personal: {
      name: '',
      contacts: [{ type: 'email', value: '' }]
    },
    education: [
      { school: '', degree: '', courses: [] }
    ]
  });

  // Immer ile güncelleme - doğrudan mutasyon yazabilirsiniz
  const updateName = (name) => {
    setFormState(produce(draft => {
      draft.personal.name = name;
    }));
  };

  const addContact = (type) => {
    setFormState(produce(draft => {
      draft.personal.contacts.push({ type, value: '' });
    }));
  };

  const addCourse = (eduIndex, course) => {
    setFormState(produce(draft => {
      draft.education[eduIndex].courses.push(course);
    }));
  };

  const removeCourse = (eduIndex, courseIndex) => {
    setFormState(produce(draft => {
      draft.education[eduIndex].courses.splice(courseIndex, 1);
    }));
  };

  return <div>{/* Form UI */}</div>;
}

Bilgi: Immer, draft proxy objesi üzerinde yapılan "mutasyonları" algılayarak aslında yeni bir immutable obje üretir. Performans kaybı minimaldir ve kod okunabilirliği büyük ölçüde artar. React ekibi de karmaşık state güncellemelerinde Immer kullanmayı önerir.

Custom Hook ile useState Pattern'leri

Tekrarlayan useState pattern'lerini custom hook'lara taşıyabilirsiniz:

// useToggle - boolean state için
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue(prev => !prev), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

// useArray - array state yönetimi için
function useArray(initialValue = []) {
  const [array, setArray] = useState(initialValue);

  const push = useCallback((item) => {
    setArray(prev => [...prev, item]);
  }, []);

  const removeAt = useCallback((index) => {
    setArray(prev => prev.filter((_, i) => i !== index));
  }, []);

  const updateAt = useCallback((index, newItem) => {
    setArray(prev => prev.map((item, i) => i === index ? newItem : item));
  }, []);

  const clear = useCallback(() => setArray([]), []);

  return { array, setArray, push, removeAt, updateAt, clear };
}

// Kullanım
function TodoApp() {
  const { value: showCompleted, toggle: toggleFilter } = useToggle(false);
  const { array: todos, push: addTodo, removeAt, updateAt } = useArray([]);

  return (
    <div>
      <button onClick={toggleFilter}>
        {showCompleted ? 'Tümünü Göster' : 'Tamamlananları Göster'}
      </button>
      <button onClick={() => addTodo({ text: 'Yeni görev', done: false })}>
        Ekle
      </button>
    </div>
  );
}

useState vs useReducer: Ne Zaman Hangisi?

İkisi arasında seçim yaparken şu kriterlere bakın:

  • useState tercih edin: Basit, bağımsız state değerleri; boolean toggle; form input'ları; sayaçlar
  • useReducer tercih edin: Birbiriyle ilişkili birden fazla state; karmaşık güncelleme mantığı; state geçişlerinin öngörülebilir olması gereken durumlar; test edilebilirlik öncelikli olduğunda

Uyarı: useState ile çok fazla ilişkili state tanımlamak, tutarsızlıklara yol açabilir. Eğer bir state güncellemesi diğer state'lerin de güncellenmesini gerektiriyorsa, useReducer'a geçmeyi düşünün.

Performans İpuçları

function OptimizedList() {
  const [items, setItems] = useState([]);
  const [filter, setFilter] = useState('');

  // Filtreleme her render'da yapılır - basit listeler için sorun değil
  const filteredItems = items.filter(item =>
    item.name.toLowerCase().includes(filter.toLowerCase())
  );

  // State'i güncellerken gereksiz render'ları önleme
  const addItem = useCallback((newItem) => {
    setItems(prev => {
      // Duplicate kontrolü
      if (prev.some(item => item.id === newItem.id)) {
        return prev; // Aynı referansı döndürmek re-render'ı önler
      }
      return [...prev, newItem];
    });
  }, []);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filtrele..."
      />
      {filteredItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Sonuç

useState basit bir API sunsa da, doğru kullanımı uygulamanızın güvenilirliğini ve performansını doğrudan etkiler. Fonksiyonel güncelleme pattern'i, lazy initialization, immutable güncelleme teknikleri ve custom hook'lar ile useState'i etkili bir şekilde kullanabilirsiniz. Karmaşık state senaryolarında Immer gibi yardımcı kütüphaneler veya useReducer hook'una geçiş yapmayı değerlendirin.

Kaynaklar

İlgili Yazılar