Ders İçeriği
Hooks Nedir? Neden Kullanılır?
React Hooks, React 16.8 sürümüyle birlikte tanıtılan ve fonksiyonel bileşenlerde state yönetimi ve yaşam döngüsü işlemlerini mümkün kılan özel fonksiyonlardır. Hooks'tan önce, state yönetimi ve yaşam döngüsü metotları sadece sınıf bileşenlerinde kullanılabiliyordu. Hooks sayesinde, fonksiyonel bileşenler de bu güçlü özelliklere sahip oldu.
Hooks'un temel avantajları:
Kod Yeniden Kullanımı: Hooks, bileşenler arasında stateful mantığı paylaşmayı kolaylaştırır. Özel hooks oluşturarak, karmaşık mantığı farklı bileşenlerde yeniden kullanabilirsiniz.
Daha Basit Kod: Fonksiyonel bileşenler, sınıf bileşenlerinden daha az kod yazımı gerektirir ve daha okunabilirdir. Hooks sayesinde, karmaşık yaşam döngüsü mantığını daha basit bir şekilde yönetebilirsiniz.
Daha İyi Test Edilebilirlik: Fonksiyonel bileşenler ve hooks, birim testleri yazmayı daha kolay hale getirir.
Performans Optimizasyonu: Hooks, React'in optimizasyon özelliklerini daha etkili kullanmanıza olanak tanır.
Temel Hooks (useState, useEffect, useContext)
useState Hook'u
useState
hook'u, fonksiyonel bileşenlerde state yönetimi için kullanılır. Daha önceki derslerde temel kullanımını görmüştük, şimdi daha detaylı inceleyelim.
Temel Kullanım
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Sayaç: {count}</p>
<button onClick={() => setCount(count + 1)}>Artır</button>
</div>
);
}
Fonksiyonel State Güncellemesi
State güncellemesi yaparken, önceki state değerine bağlı olarak güncelleme yapacaksanız, fonksiyonel güncelleme kullanmalısınız:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// Yanlış yaklaşım - race condition'a neden olabilir
// setCount(count + 1);
// Doğru yaklaşım - fonksiyonel güncelleme
setCount(prevCount => prevCount + 1);
};
const incrementTwice = () => {
// Bu şekilde iki kez artırım garantilenir
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Sayaç: {count}</p>
<button onClick={increment}>Bir Artır</button>
<button onClick={incrementTwice}>İki Artır</button>
</div>
);
}
Karmaşık State Yönetimi
Karmaşık objeler ve diziler için useState kullanımı:
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true
}
});
const updateName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
const updatePreferences = (key, value) => {
setUser(prevUser => ({
...prevUser,
preferences: {
...prevUser.preferences,
[key]: value
}
}));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
placeholder="İsim"
/>
<label>
<input
type="checkbox"
checked={user.preferences.notifications}
onChange={(e) => updatePreferences('notifications', e.target.checked)}
/>
Bildirimleri etkinleştir
</label>
<select
value={user.preferences.theme}
onChange={(e) => updatePreferences('theme', e.target.value)}
>
<option value="light">Açık Tema</option>
<option value="dark">Koyu Tema</option>
</select>
</div>
);
}
Lazy Initial State
State'in başlangıç değeri hesaplaması maliyetli ise, lazy initialization kullanabilirsiniz:
function ExpensiveComponent() {
// Bu fonksiyon sadece ilk render'da çalışır
const [data, setData] = useState(() => {
console.log('Pahalı hesaplama yapılıyor...');
return Array.from({ length: 1000 }, (_, i) => ({
id: i,
value: Math.random()
}));
});
return (
<div>
<p>Veri sayısı: {data.length}</p>
<button onClick={() => setData([])}>Temizle</button>
</div>
);
}
useEffect Hook'u (Yaşam Döngüsü Metotları)
useEffect
hook'u, fonksiyonel bileşenlerde yan etkiler (side effects) gerçekleştirmek için kullanılır. Bu yan etkiler arasında API çağrıları, DOM manipülasyonu, zamanlayıcılar ve temizlik işlemleri yer alır.
Temel useEffect Kullanımı
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Bu kod her render'dan sonra çalışır
console.log('Bileşen render edildi');
// API çağrısı
fetchUser(userId)
.then(userData => {
setUser(userData);
setLoading(false);
})
.catch(error => {
console.error('Kullanıcı yüklenemedi:', error);
setLoading(false);
});
});
if (loading) return <div>Yükleniyor...</div>;
if (!user) return <div>Kullanıcı bulunamadı</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Dependency Array ile useEffect
useEffect'in ikinci parametresi olan dependency array, effect'in ne zaman çalışacağını kontrol eder:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(userData => {
setUser(userData);
setLoading(false);
});
}, [userId]); // Sadece userId değiştiğinde çalışır
return (
<div>
{loading ? (
<div>Yükleniyor...</div>
) : (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
</div>
);
}
Cleanup Fonksiyonu
useEffect'ten bir fonksiyon döndürerek temizlik işlemleri yapabilirsiniz:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Cleanup fonksiyonu
return () => {
clearInterval(interval);
};
}, []); // Boş dependency array - sadece mount/unmount'ta çalışır
return <div>Geçen süre: {seconds} saniye</div>;
}
Birden Fazla useEffect
Farklı amaçlar için birden fazla useEffect kullanabilirsiniz:
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [onlineStatus, setOnlineStatus] = useState(false);
// Kullanıcı bilgilerini yükle
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Kullanıcının gönderilerini yükle
useEffect(() => {
fetchUserPosts(userId).then(setPosts);
}, [userId]);
// Online durumu takip et
useEffect(() => {
const subscription = subscribeToUserStatus(userId, setOnlineStatus);
return () => {
subscription.unsubscribe();
};
}, [userId]);
return (
<div>
<h2>{user?.name} {onlineStatus ? '🟢' : '🔴'}</h2>
<p>Gönderi sayısı: {posts.length}</p>
</div>
);
}
useEffect ile Yaşam Döngüsü Simülasyonu
Sınıf bileşenlerindeki yaşam döngüsü metotlarını useEffect ile simüle edebilirsiniz:
function LifecycleDemo({ prop }) {
const [state, setState] = useState(0);
// componentDidMount
useEffect(() => {
console.log('Bileşen mount edildi');
// componentWillUnmount
return () => {
console.log('Bileşen unmount edilecek');
};
}, []);
// componentDidUpdate (sadece prop değiştiğinde)
useEffect(() => {
console.log('Prop değişti:', prop);
}, [prop]);
// componentDidUpdate (sadece state değiştiğinde)
useEffect(() => {
console.log('State değişti:', state);
}, [state]);
return (
<div>
<p>State: {state}</p>
<button onClick={() => setState(state + 1)}>State'i Artır</button>
</div>
);
}
useContext Hook'u (Global State Yönetimi)
useContext
hook'u, React Context API'sini fonksiyonel bileşenlerde kullanmak için kullanılır. Context, bileşen ağacında props drilling yapmadan veri paylaşmayı sağlar.
Context Oluşturma ve Kullanma
import React, { createContext, useContext, useState } from 'react';
// Context oluşturma
const ThemeContext = createContext();
// Provider bileşeni
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// useContext hook'u ile context kullanma
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}>
<h1>Benim Uygulamam</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? 'Koyu' : 'Açık'} Tema
</button>
</header>
);
}
function Content() {
const { theme } = useContext(ThemeContext);
return (
<main style={{
backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px'
}}>
<p>Bu içerik {theme} temasında görüntüleniyor.</p>
</main>
);
}
// Ana uygulama
function App() {
return (
<ThemeProvider>
<div>
<Header />
<Content />
</div>
</ThemeProvider>
);
}
Karmaşık State Yönetimi için Context
Daha karmaşık state yönetimi için useReducer ile birlikte kullanılabilir:
import React, { createContext, useContext, useReducer } from 'react';
// Action types
const ACTIONS = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO',
SET_FILTER: 'SET_FILTER'
};
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TODO:
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case ACTIONS.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case ACTIONS.DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case ACTIONS.SET_FILTER:
return {
...state,
filter: action.payload
};
default:
return state;
}
}
// Context
const TodoContext = createContext();
// Provider
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
});
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// Custom hook
function useTodos() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodos must be used within TodoProvider');
}
return context;
}
// Bileşenler
function TodoForm() {
const [text, setText] = useState('');
const { dispatch } = useTodos();
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: ACTIONS.ADD_TODO, payload: text });
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Yeni görev ekle"
/>
<button type="submit">Ekle</button>
</form>
);
}
function TodoList() {
const { state, dispatch } = useTodos();
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'completed') return todo.completed;
if (state.filter === 'active') return !todo.completed;
return true;
});
return (
<div>
<div>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'all' })}>
Tümü
</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'active' })}>
Aktif
</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'completed' })}>
Tamamlanan
</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer'
}}
onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: todo.id })}
>
{todo.text}
</span>
<button
onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: todo.id })}
>
Sil
</button>
</li>
))}
</ul>
</div>
);
}
Özel Hooks Oluşturma
Özel hooks, stateful mantığı bileşenler arasında paylaşmanın güçlü bir yoludır. Özel hook'lar, "use" ile başlayan ve diğer hook'ları çağırabilen fonksiyonlardır.
Basit Özel Hook
// useCounter özel hook'u
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Kullanımı
function Counter() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<p>Sayaç: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Sıfırla</button>
</div>
);
}
API Çağrısı için Özel Hook
// useApi özel hook'u
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Kullanımı
function UserProfile({ userId }) {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>Yükleniyor...</div>;
if (error) return <div>Hata: {error}</div>;
if (!user) return <div>Kullanıcı bulunamadı</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Local Storage için Özel Hook
// useLocalStorage özel hook'u
function useLocalStorage(key, initialValue) {
// State'i lazy initialization ile başlat
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Değeri state ve localStorage'a kaydet
const setValue = (value) => {
try {
// Fonksiyon olarak da değer alabilir
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
// Kullanımı
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'tr');
return (
<div>
<label>
Tema:
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Açık</option>
<option value="dark">Koyu</option>
</select>
</label>
<label>
Dil:
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="tr">Türkçe</option>
<option value="en">English</option>
</select>
</label>
</div>
);
}
Form Yönetimi için Özel Hook
// useForm özel hook'u
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Validation
if (validate) {
const fieldErrors = validate({ ...values, [name]: value });
setErrors(prev => ({ ...prev, [name]: fieldErrors[name] }));
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
// Tüm alanları touched olarak işaretle
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);
// Validation
if (validate) {
const formErrors = validate(values);
setErrors(formErrors);
// Hata varsa submit etme
if (Object.keys(formErrors).some(key => formErrors[key])) {
return;
}
}
onSubmit(values);
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// Kullanımı
function ContactForm() {
const initialValues = {
name: '',
email: '',
message: ''
};
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = 'İsim gerekli';
}
if (!values.email) {
errors.email = 'E-posta gerekli';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Geçersiz e-posta formatı';
}
if (!values.message) {
errors.message = 'Mesaj gerekli';
}
return errors;
};
const { values, errors, touched, handleChange, handleBlur, handleSubmit, reset } =
useForm(initialValues, validate);
const onSubmit = (formData) => {
console.log('Form gönderildi:', formData);
// API çağrısı yapılabilir
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
type="text"
placeholder="İsim"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{touched.name && errors.name && (
<span style={{ color: 'red' }}>{errors.name}</span>
)}
</div>
<div>
<input
type="email"
placeholder="E-posta"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
</div>
<div>
<textarea
placeholder="Mesaj"
value={values.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={() => handleBlur('message')}
/>
{touched.message && errors.message && (
<span style={{ color: 'red' }}>{errors.message}</span>
)}
</div>
<button type="submit">Gönder</button>
<button type="button" onClick={reset}>Temizle</button>
</form>
);
}