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.