Ders İçeriği
Neden State Yönetimi?
React uygulamaları büyüdükçe, bileşenler arasında veri paylaşımı karmaşık hale gelir. Küçük uygulamalarda props drilling (props'ları birden fazla seviye aşağı geçirme) yeterli olabilir, ancak büyük uygulamalarda bu yaklaşım sürdürülebilir değildir.
State yönetiminin gerekli olduğu durumlar şunlardır: kullanıcı kimlik doğrulama bilgileri, tema tercihleri, alışveriş sepeti içeriği, form verileri, API'den gelen veriler ve uygulama genelinde kullanılan ayarlar. Bu tür verilerin birden fazla bileşen tarafından kullanılması gerektiğinde, global state yönetimi devreye girer.
Global state yönetimi, uygulamanın herhangi bir yerinden erişilebilen ve değiştirilebilen merkezi bir veri deposu sağlar. Bu yaklaşım, kodun daha temiz, sürdürülebilir ve test edilebilir olmasını sağlar.
React ekosisteminde state yönetimi için çeşitli çözümler bulunur. En popüler olanları Context API (React'in yerleşik çözümü), Redux, Zustand, Recoil ve Jotai'dir. Bu derste Context API ve Redux üzerinde duracağız.
Context API ile Global State Yönetimi
Context API, React'in yerleşik state yönetimi çözümüdür. Props drilling problemini çözmek için tasarlanmıştır ve küçük-orta ölçekli uygulamalar için yeterlidir.
Temel Context Kullanımı
Basit bir tema yönetimi örneği ile başlayalım:
import React, { createContext, useContext, useState } from 'react';
// Context oluşturma
const ThemeContext = createContext();
// Provider bileşeni
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
colors: {
light: {
background: '#ffffff',
text: '#000000',
primary: '#007bff'
},
dark: {
background: '#121212',
text: '#ffffff',
primary: '#bb86fc'
}
}
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Bileşenlerde kullanım
function Header() {
const { theme, toggleTheme, colors } = useTheme();
const currentColors = colors[theme];
return (
<header style={{
backgroundColor: currentColors.background,
color: currentColors.text,
padding: '1rem'
}}>
<h1>Benim Uygulamam</h1>
<button
onClick={toggleTheme}
style={{
backgroundColor: currentColors.primary,
color: currentColors.background,
border: 'none',
padding: '0.5rem 1rem',
borderRadius: '4px'
}}
>
{theme === 'light' ? 'Koyu Tema' : 'Açık Tema'}
</button>
</header>
);
}
function Content() {
const { theme, colors } = useTheme();
const currentColors = colors[theme];
return (
<main style={{
backgroundColor: currentColors.background,
color: currentColors.text,
padding: '2rem',
minHeight: '80vh'
}}>
<h2>İçerik Alanı</h2>
<p>Mevcut tema: {theme}</p>
</main>
);
}
// Ana uygulama
function App() {
return (
<ThemeProvider>
<div>
<Header />
<Content />
</div>
</ThemeProvider>
);
}
Karmaşık State Yönetimi için useReducer
Daha karmaşık state mantığı için useReducer hook'u ile Context API'yi birleştirmek etkili bir yaklaşımdır:
import React, { createContext, useContext, useReducer } from 'react';
// Action types
const ACTIONS = {
LOGIN: 'LOGIN',
LOGOUT: 'LOGOUT',
UPDATE_PROFILE: 'UPDATE_PROFILE',
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR',
CLEAR_ERROR: 'CLEAR_ERROR'
};
// Initial state
const initialState = {
user: null,
isAuthenticated: false,
loading: false,
error: null
};
// Reducer function
function authReducer(state, action) {
switch (action.type) {
case ACTIONS.SET_LOADING:
return {
...state,
loading: action.payload,
error: null
};
case ACTIONS.SET_ERROR:
return {
...state,
loading: false,
error: action.payload
};
case ACTIONS.CLEAR_ERROR:
return {
...state,
error: null
};
case ACTIONS.LOGIN:
return {
...state,
user: action.payload,
isAuthenticated: true,
loading: false,
error: null
};
case ACTIONS.LOGOUT:
return {
...state,
user: null,
isAuthenticated: false,
loading: false,
error: null
};
case ACTIONS.UPDATE_PROFILE:
return {
...state,
user: {
...state.user,
...action.payload
},
loading: false,
error: null
};
default:
return state;
}
}
// Context
const AuthContext = createContext();
// Provider
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, initialState);
// Action creators
const login = async (credentials) => {
try {
dispatch({ type: ACTIONS.SET_LOADING, payload: true });
// API çağrısı simülasyonu
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Giriş başarısız');
}
const userData = await response.json();
// Token'ı localStorage'a kaydet
localStorage.setItem('authToken', userData.token);
dispatch({ type: ACTIONS.LOGIN, payload: userData.user });
} catch (error) {
dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
throw error;
}
};
const logout = () => {
localStorage.removeItem('authToken');
dispatch({ type: ACTIONS.LOGOUT });
};
const updateProfile = async (profileData) => {
try {
dispatch({ type: ACTIONS.SET_LOADING, payload: true });
const response = await fetch('/api/user/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify(profileData)
});
if (!response.ok) {
throw new Error('Profil güncellenemedi');
}
const updatedUser = await response.json();
dispatch({ type: ACTIONS.UPDATE_PROFILE, payload: updatedUser });
} catch (error) {
dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
throw error;
}
};
const clearError = () => {
dispatch({ type: ACTIONS.CLEAR_ERROR });
};
// Token kontrolü (uygulama başlangıcında)
useEffect(() => {
const token = localStorage.getItem('authToken');
if (token) {
// Token geçerliliğini kontrol et
verifyToken(token);
}
}, []);
const verifyToken = async (token) => {
try {
dispatch({ type: ACTIONS.SET_LOADING, payload: true });
const response = await fetch('/api/auth/verify', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
dispatch({ type: ACTIONS.LOGIN, payload: userData });
} else {
localStorage.removeItem('authToken');
dispatch({ type: ACTIONS.LOGOUT });
}
} catch (error) {
localStorage.removeItem('authToken');
dispatch({ type: ACTIONS.LOGOUT });
}
};
const value = {
...state,
login,
logout,
updateProfile,
clearError
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Kullanım örnekleri
function LoginForm() {
const [credentials, setCredentials] = useState({ email: '', password: '' });
const { login, loading, error, clearError } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login(credentials);
// Başarılı giriş sonrası yönlendirme
window.location.href = '/dashboard';
} catch (error) {
// Hata zaten context'te yönetiliyor
}
};
useEffect(() => {
return () => clearError(); // Component unmount olduğunda hataları temizle
}, [clearError]);
return (
<form onSubmit={handleSubmit}>
<h2>Giriş Yap</h2>
{error && (
<div style={{ color: 'red', marginBottom: '1rem' }}>
{error}
</div>
)}
<div>
<label>
E-posta:
<input
type="email"
value={credentials.email}
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
required
/>
</label>
</div>
<div>
<label>
Şifre:
<input
type="password"
value={credentials.password}
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
required
/>
</label>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Giriş yapılıyor...' : 'Giriş Yap'}
</button>
</form>
);
}
function UserProfile() {
const { user, isAuthenticated, updateProfile, loading } = useAuth();
const [profileData, setProfileData] = useState({
name: '',
email: '',
phone: ''
});
useEffect(() => {
if (user) {
setProfileData({
name: user.name || '',
email: user.email || '',
phone: user.phone || ''
});
}
}, [user]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await updateProfile(profileData);
alert('Profil güncellendi!');
} catch (error) {
alert('Profil güncellenemedi!');
}
};
if (!isAuthenticated) {
return <div>Lütfen giriş yapın.</div>;
}
return (
<form onSubmit={handleSubmit}>
<h2>Profil Bilgileri</h2>
<div>
<label>
İsim:
<input
type="text"
value={profileData.name}
onChange={(e) => setProfileData({...profileData, name: e.target.value})}
/>
</label>
</div>
<div>
<label>
E-posta:
<input
type="email"
value={profileData.email}
onChange={(e) => setProfileData({...profileData, email: e.target.value})}
/>
</label>
</div>
<div>
<label>
Telefon:
<input
type="tel"
value={profileData.phone}
onChange={(e) => setProfileData({...profileData, phone: e.target.value})}
/>
</label>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Güncelleniyor...' : 'Profili Güncelle'}
</button>
</form>
);
}
Birden Fazla Context Yönetimi
Büyük uygulamalarda farklı amaçlar için birden fazla context kullanmak yaygındır:
// Contexts'leri birleştiren provider
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
<ShoppingCartProvider>
{children}
</ShoppingCartProvider>
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
// Ana uygulama
function App() {
return (
<AppProviders>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Router>
</AppProviders>
);
}
Redux Nedir? Temel Kavramlar
Redux, JavaScript uygulamaları için öngörülebilir state container'ıdır. React ile birlikte sık kullanılsa da, framework-agnostic'tir ve herhangi bir JavaScript uygulamasında kullanılabilir.
Redux'un temel prensipleri şunlardır:
Single Source of Truth: Uygulamanın tüm state'i tek bir store'da tutulur. Bu, state'in öngörülebilir olmasını ve debug edilmesini kolaylaştırır.
State is Read-Only: State'i değiştirmenin tek yolu action dispatch etmektir. Bu, state değişikliklerinin izlenebilir olmasını sağlar.
Changes are Made with Pure Functions: State değişiklikleri reducer fonksiyonları ile yapılır. Reducer'lar pure function'lardır ve aynı input için her zaman aynı output'u verirler.
Redux Temel Kavramları
Store: Uygulamanın state'ini tutan obje. Tek bir store bulunur ve tüm state burada tutulur.
Action: State'te yapılacak değişikliği tanımlayan plain JavaScript objesi. Her action'ın bir type
property'si olmalıdır.
Reducer: Action'ları alıp yeni state döndüren pure function. Mevcut state'i değiştirmez, yeni state objesi döndürür.
Dispatch: Action'ları store'a gönderme işlemi. Store, action'ı ilgili reducer'a iletir.
Redux Kurulumu ve Temel Kullanım
bash npm install @reduxjs/toolkit react-redux
Redux Toolkit, Redux'un modern ve önerilen kullanım şeklidir. Daha az boilerplate kod gerektirir ve best practice'leri varsayılan olarak uygular.
// store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
history: []
},
reducers: {
increment: (state) => {
state.value += 1;
state.history.push({ action: 'increment', timestamp: Date.now() });
},
decrement: (state) => {
state.value -= 1;
state.history.push({ action: 'decrement', timestamp: Date.now() });
},
incrementByAmount: (state, action) => {
state.value += action.payload;
state.history.push({
action: 'incrementByAmount',
amount: action.payload,
timestamp: Date.now()
});
},
reset: (state) => {
state.value = 0;
state.history.push({ action: 'reset', timestamp: Date.now() });
}
}
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
// store/slices/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async (_, { rejectWithValue }) => {
try {
const response = await fetch('/api/todos');
if (!response.ok) {
throw new Error('Failed to fetch todos');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const addTodo = createAsyncThunk(
'todos/addTodo',
async (todoText, { rejectWithValue }) => {
try {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: todoText, completed: false })
});
if (!response.ok) {
throw new Error('Failed to add todo');
}
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const todoSlice = createSlice({
name: 'todos',
initialState: {
items: [],
loading: false,
error: null,
filter: 'all' // all, active, completed
},
reducers: {
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
},
clearCompleted: (state) => {
state.items = state.items.filter(item => !item.completed);
}
},
extraReducers: (builder) => {
builder
// Fetch todos
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// Add todo
.addCase(addTodo.pending, (state) => {
state.loading = true;
})
.addCase(addTodo.fulfilled, (state, action) => {
state.loading = false;
state.items.push(action.payload);
})
.addCase(addTodo.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export const { toggleTodo, deleteTodo, setFilter, clearCompleted } = todoSlice.actions;
export default todoSlice.reducer;
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
import todoReducer from './slices/todoSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
})
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// hooks/redux.js
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './components/Counter';
import TodoApp from './components/TodoApp';
function App() {
return (
<Provider store={store}>
<div className="App">
<Counter />
<TodoApp />
</div>
</Provider>
);
}
export default App;
// components/Counter.js
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
import { increment, decrement, incrementByAmount, reset } from '../store/slices/counterSlice';
function Counter() {
const count = useAppSelector(state => state.counter.value);
const history = useAppSelector(state => state.counter.history);
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState(5);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}>
<h2>Counter: {count}</h2>
<div style={{ marginBottom: '10px' }}>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())} style={{ marginLeft: '10px' }}>-</button>
<button onClick={() => dispatch(reset())} style={{ marginLeft: '10px' }}>Reset</button>
</div>
<div style={{ marginBottom: '10px' }}>
<input
type="number"
value={incrementAmount}
onChange={(e) => setIncrementAmount(Number(e.target.value))}
/>
<button
onClick={() => dispatch(incrementByAmount(incrementAmount))}
style={{ marginLeft: '10px' }}
>
Add Amount
</button>
</div>
<div>
<h3>History:</h3>
<ul style={{ maxHeight: '100px', overflowY: 'auto' }}>
{history.slice(-5).map((entry, index) => (
<li key={index}>
{entry.action} {entry.amount && `(${entry.amount})`} - {new Date(entry.timestamp).toLocaleTimeString()}
</li>
))}
</ul>
</div>
</div>
);
}
export default Counter;
// components/TodoApp.js
import React, { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
import {
fetchTodos,
addTodo,
toggleTodo,
deleteTodo,
setFilter,
clearCompleted
} from '../store/slices/todoSlice';
function TodoApp() {
const { items, loading, error, filter } = useAppSelector(state => state.todos);
const dispatch = useAppDispatch();
const [newTodoText, setNewTodoText] = useState('');
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
const handleAddTodo = async (e) => {
e.preventDefault();
if (newTodoText.trim()) {
await dispatch(addTodo(newTodoText.trim()));
setNewTodoText('');
}
};
const filteredTodos = items.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const activeCount = items.filter(todo => !todo.completed).length;
const completedCount = items.filter(todo => todo.completed).length;
return (
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}>
<h2>Todo App</h2>
{error && (
<div style={{ color: 'red', marginBottom: '10px' }}>
Error: {error}
</div>
)}
<form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new todo..."
disabled={loading}
/>
<button type="submit" disabled={loading || !newTodoText.trim()}>
{loading ? 'Adding...' : 'Add Todo'}
</button>
</form>
<div style={{ marginBottom: '20px' }}>
<button
onClick={() => dispatch(setFilter('all'))}
style={{
backgroundColor: filter === 'all' ? '#007bff' : '#f8f9fa',
color: filter === 'all' ? 'white' : 'black'
}}
>
All ({items.length})
</button>
<button
onClick={() => dispatch(setFilter('active'))}
style={{
marginLeft: '10px',
backgroundColor: filter === 'active' ? '#007bff' : '#f8f9fa',
color: filter === 'active' ? 'white' : 'black'
}}
>
Active ({activeCount})
</button>
<button
onClick={() => dispatch(setFilter('completed'))}
style={{
marginLeft: '10px',
backgroundColor: filter === 'completed' ? '#007bff' : '#f8f9fa',
color: filter === 'completed' ? 'white' : 'black'
}}
>
Completed ({completedCount})
</button>
{completedCount > 0 && (
<button
onClick={() => dispatch(clearCompleted())}
style={{ marginLeft: '20px', color: 'red' }}
>
Clear Completed
</button>
)}
</div>
{loading && <div>Loading todos...</div>}
<ul style={{ listStyle: 'none', padding: 0 }}>
{filteredTodos.map(todo => (
<li
key={todo.id}
style={{
padding: '10px',
borderBottom: '1px solid #eee',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
style={{ marginRight: '10px' }}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#666' : 'black'
}}
>
{todo.text}
</span>
</div>
<button
onClick={() => dispatch(deleteTodo(todo.id))}
style={{ color: 'red', border: 'none', background: 'none', cursor: 'pointer' }}
>
Delete
</button>
</li>
))}
</ul>
{filteredTodos.length === 0 && !loading && (
<div style={{ textAlign: 'center', color: '#666', marginTop: '20px' }}>
{filter === 'all' ? 'No todos yet' : `No ${filter} todos`}
</div>
)}
</div>
);
}
export default TodoApp;
Redux, büyük ve karmaşık uygulamalar için güçlü bir state yönetimi çözümüdür. Context API ise küçük-orta ölçekli uygulamalar için yeterlidir. Hangi yaklaşımı seçeceğiniz, uygulamanızın karmaşıklığına, takım büyüklüğüne ve gereksinimlere bağlıdır. Her iki yaklaşımın da avantajları ve dezavantajları vardır, bu nedenle proje gereksinimlerinizi dikkatlice değerlendirerek karar vermeniz önemlidir.