React'te State Yönetimi: Temel Kavramlar ve Uygulamalar

React'te State Yönetimi: Temel Kavramlar ve Uygulamalar
State (durum), React bileşenlerinin dinamik ve etkileşimli olmasını sağlayan temel kavramdır. Kullanıcı etkileşimleri, API yanıtları veya zamanlayıcılar gibi olaylara bağlı olarak değişen verileri temsil eder. Bu yazıda React'te state yönetiminin temellerini, state tasarım prensiplerini ve yaygın hataları detaylı olarak inceleyeceğiz.
State vs Props: Temel Fark
State ve props arasındaki farkı anlamak React'in en önemli kavramlarından biridir:
| Özellik | State | Props |
|---|---|---|
| Sahiplik | Bileşenin kendisi yönetir | Üst bileşen tarafından aktarılır |
| Değiştirilebilirlik | setState ile değiştirilebilir | Salt okunur (read-only) |
| Amaç | Bileşenin iç durumunu yönetmek | Bileşeni yapılandırmak |
| Değiştiğinde | Re-render tetiklenir | Re-render tetiklenir |
State, bir bileşenin "hafızası"dır. Render'lar arasında bilgiyi korur ve değiştiğinde bileşenin güncellenmesini sağlar.
useState ile State Tanımlama
Modern React'te state yönetiminin en temel aracı useState hook'udur:
import { useState } from 'react';
function RegistrationForm() {
// Farklı türlerde state tanımlama
const [name, setName] = useState(''); // string
const [age, setAge] = useState(0); // number
const [isAgreed, setIsAgreed] = useState(false); // boolean
const [hobbies, setHobbies] = useState([]); // array
const [address, setAddress] = useState({ // object
city: '',
district: '',
zipCode: ''
});
const handleAddressChange = (field, value) => {
// Object state güncelleme - spread operatörü ile
setAddress(prev => ({
...prev,
[field]: value
}));
};
const addHobby = (hobby) => {
// Array state güncelleme - spread operatörü ile
setHobbies(prev => [...prev, hobby]);
};
const removeHobby = (index) => {
setHobbies(prev => prev.filter((_, i) => i !== index));
};
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Adınız"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Yaşınız"
/>
<input
value={address.city}
onChange={(e) => handleAddressChange('city', e.target.value)}
placeholder="Şehir"
/>
<label>
<input
type="checkbox"
checked={isAgreed}
onChange={(e) => setIsAgreed(e.target.checked)}
/>
Koşulları kabul ediyorum
</label>
</form>
);
}
State Güncelleme Kuralları
1. State Güncellemeleri Asenkrondur
React, performans için state güncellemelerini toplu (batch) olarak işler:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// YANLIŞ: Her ikisi de aynı count değerini kullanır
setCount(count + 1); // count hala 0
setCount(count + 1); // count hala 0 - sonuç: 1
// DOĞRU: Fonksiyonel güncelleme ile önceki değeri kullan
setCount(prev => prev + 1); // 0 + 1 = 1
setCount(prev => prev + 1); // 1 + 1 = 2 - sonuç: 2
};
return (
<div>
<p>Sayaç: {count}</p>
<button onClick={handleClick}>+2 Artır</button>
</div>
);
}
Uyarı: State güncelleme fonksiyonu çağrıldıktan hemen sonra state değeri hala eski değeri korur. Yeni değer bir sonraki render'da kullanılabilir. Bu nedenle güncellenmiş değere bağlı işlemlerde fonksiyonel güncelleme pattern'ini kullanın.
2. State'i Doğrudan Mutate Etmeyin
function TodoManager() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React öğren', done: false }
]);
// YANLIŞ - doğrudan mutasyon
const wrongToggle = (id) => {
const todo = todos.find(t => t.id === id);
todo.done = !todo.done; // Doğrudan mutasyon!
setTodos(todos); // Aynı referans - React değişikliği algılamaz
};
// DOĞRU - yeni referans oluşturma
const correctToggle = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
// Ekleme
const addTodo = (text) => {
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
};
// Silme
const removeTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
// Güncelleme
const updateTodo = (id, newText) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
)
);
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => correctToggle(todo.id)}>Tamamla</button>
<button onClick={() => removeTodo(todo.id)}>Sil</button>
</li>
))}
</ul>
);
}
State Tasarım Prensipleri
1. Derived State Kullanmayın
Mevcut state'ten hesaplanabilecek değerleri ayrı bir state olarak tanımlamayın:
function ShoppingCart() {
const [items, setItems] = useState([]);
// YANLIŞ: totalPrice ayrı bir state olarak tanımlamak
// const [totalPrice, setTotalPrice] = useState(0);
// DOĞRU: Mevcut state'ten hesapla (derived value)
const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const hasItems = items.length > 0;
return (
<div>
<h3>Sepetim ({itemCount} ürün)</h3>
{items.map(item => (
<div key={item.id}>
{item.name} - {item.price} TL x {item.quantity}
</div>
))}
{hasItems && <p><strong>Toplam: {totalPrice.toFixed(2)} TL</strong></p>}
</div>
);
}
İpucu: Bir değer mevcut state veya props'tan hesaplanabiliyorsa onu ayrı bir state olarak tanımlamayın. Bu, state senkronizasyon sorunlarını önler ve kodunuzu basitleştirir. Hesaplama maliyetli ise useMemo kullanabilirsiniz.
2. State'i Mümkün Olduğunca Düz Tutun
// KÖTÜ - iç içe yapı
const [formData, setFormData] = useState({
user: {
personal: {
name: '',
email: ''
},
preferences: {
theme: 'light',
language: 'tr'
}
}
});
// İYİ - düz yapı
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [theme, setTheme] = useState('light');
const [language, setLanguage] = useState('tr');
State Lifting (State'i Yukarı Taşıma)
İki kardeş bileşenin aynı state'i paylaşması gerektiğinde, state ortak üst bileşene taşınır:
function TemperatureConverter() {
const [celsius, setCelsius] = useState('');
const fahrenheit = celsius !== ''
? ((parseFloat(celsius) * 9/5) + 32).toFixed(1)
: '';
return (
<div>
<h3>Sıcaklık Dönüştürücü</h3>
<TemperatureInput
label="Celsius"
value={celsius}
onChange={setCelsius}
/>
<TemperatureInput
label="Fahrenheit"
value={fahrenheit}
onChange={(f) => {
const c = ((parseFloat(f) - 32) * 5/9).toFixed(1);
setCelsius(c);
}}
/>
</div>
);
}
function TemperatureInput({ label, value, onChange }) {
return (
<label>
{label}:
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}
Bilgi: React 18 ile birlikte state güncellemeleri artık tüm durumlarda (event handler'lar, setTimeout, Promise'ler) otomatik olarak batch (toplu) olarak işlenir. Bu, gereksiz re-render'ları azaltarak performansı artırır.
Lazy Initialization
useState'e başlangıç değeri olarak maliyetli bir hesaplama gerekiyorsa, fonksiyon geçirin:
function ExpensiveComponent() {
// YANLIŞ: Her render'da hesaplanır
// const [data, setData] = useState(expensiveCalculation());
// DOĞRU: Yalnızca ilk render'da hesaplanır
const [data, setData] = useState(() => {
const saved = localStorage.getItem('appData');
return saved ? JSON.parse(saved) : { items: [], settings: {} };
});
return <div>{/* ... */}</div>;
}
Sonuç
State yönetimi, React uygulamalarının kalbidir. Doğru state tasarımı, uygulamanızın performansını, bakım kolaylığını ve hata ayıklama sürecini doğrudan etkiler. State'i immutable tutmak, derived state'ten kaçınmak, state'i mümkün olduğunca düz yapıda tutmak ve fonksiyonel güncelleme pattern'ini kullanmak en temel en iyi uygulamalardır. Daha karmaşık state senaryoları için useReducer hook'unu ve Context API'yi incelemenizi öneriyorum.