React'te Güçlü Yerel State Yönetimi: useReducer ile Karmaşık Bileşenlerinizi Basitleştirin ve Kontrol Edin
Modern React uygulamaları, dinamik ve interaktif kullanıcı arayüzleri oluşturmamıza olanak tanır. Ancak, bir bileşenin içindeki state (durum) karmaşıklaştıkça, özellikle birçok farklı eylemin durumu değiştirebildiği senaryolarda, useState hook'u ile yönetmek zorlaşabilir. Birden fazla useState çağrısı, state güncelleme mantığının dağınık hale gelmesine ve hata ayıklamanın güçleşmesine yol açabilir.
Benim geliştirme tecrübelerimde, özellikle formlar, alışveriş sepetleri veya adım adım ilerleyen sihirbazlar gibi birden fazla, birbiriyle ilişkili state parçacığını içeren bileşenlerde, useState'in sınırlarına hızla ulaştığımı gördüm. İşte bu noktada, React'in güçlü bir alternatifi olan useReducer hook'u devreye giriyor. Bu yazıda, useReducer'ın ne olduğunu, ne zaman kullanılacağını ve karmaşık yerel state yönetiminizi nasıl basitleştirebileceğini derinlemesine inceleyeceğiz. Amacımız, daha öngörülebilir, test edilebilir ve bakımı kolay React bileşenleri oluşturmanıza yardımcı olmak.
useState Neden Bazen Yetersiz Kalır?
useState, basit state yönetimi için harika bir araçtır. Ancak bir bileşenin state'i karmaşık bir yapıya büründüğünde veya state'i güncelleyen mantık arttığında, bazı sorunlar ortaya çıkabilir:
- Dağınık Güncelleme Mantığı: Birçok
useStateçağrısı arasında state güncelleme mantığı dağılabilir, bu da kodu okumayı ve anlamayı zorlaştırır. - Birbiriyle İlişkili State Parçaları: Bir state parçasındaki değişiklik, başka bir state parçasını da etkilemesi gerekiyorsa, bunu
useStateile yönetmek çok sayıdasetStateçağrısına ve mantık karmaşasına yol açar. - Test Edilebilirlik Zorluğu: State güncelleme mantığı doğrudan bileşenin içinde olduğunda, bu mantığı bileşenden bağımsız olarak test etmek güçleşir.
- Gecikmiş Güncellemeler:
useState'in asenkron doğası, birden fazla güncellemeyi aynı anda yapmaya çalışırken beklenmeyen sonuçlara yol açabilir.
Bu gibi durumlarda, Redux gibi global state yönetim çözümlerine yönelmek yerine, bileşen düzeyinde daha robust bir çözüm arayışına gireriz. useReducer tam da bu ihtiyacı karşılar.
useReducer Nedir?
useReducer, React'te state yönetimini bir reducer fonksiyonu aracılığıyla gerçekleştiren bir hook'tur. Reducer fonksiyonu, mevcut state'i ve bir action nesnesini alarak yeni bir state döndürür. Bu patern, Redux'tan tanıdık gelebilir ve aslında onun basitleştirilmiş bir yerel uygulamasıdır. useReducer bize üç ana kavram sunar:
- State: Yönetmek istediğimiz veri.
- Dispatch Fonksiyonu: State'i değiştirmek için kullanılan bir fonksiyon. Bir action nesnesini argüman olarak alır.
- Reducer Fonksiyonu: Mevcut state ve gelen action'a göre yeni state'i hesaplayan pure (saf) bir fonksiyondur.
Temel kullanımı şu şekildedir:
const [state, dispatch] = useReducer(reducer, initialState);Burada reducer, state'i güncelleyen fonksiyon, initialState ise başlangıç state'idir. state mevcut durumu, dispatch ise eylemleri tetiklemek için kullanılan fonksiyonu döndürür.
Neden Karmaşık Yerel State İçin useReducer?
useReducer'ı karmaşık yerel state yönetimi için tercih etmemizin başlıca nedenleri şunlardır:
Merkezi State Mantığı: Tüm state güncelleme mantığı, bileşenin dışındaki (veya bileşenin içinde ayrı bir fonksiyon olarak tanımlanmış) tek bir
reducerfonksiyonunda toplanır. Bu, kodun okunabilirliğini ve bakımını artırır.Öngörülebilirlik: Reducer fonksiyonu saf (pure) bir fonksiyondur; yani aynı girdi (state ve action) için her zaman aynı çıktıyı (yeni state) döndürür ve yan etkisi (side effect) yoktur. Bu da uygulamanızın davranışını çok daha öngörülebilir hale getirir. Hatırlarsanız, JavaScript ve Node.js'te Tasarım Desenleri yazımda da saf fonksiyonların ve tasarım prensiplerinin öneminden bahsetmiştim.
Daha İyi Test Edilebilirlik: Reducer fonksiyonu, herhangi bir React bileşeninden bağımsız olarak saf bir JavaScript fonksiyonu olduğu için kolayca test edilebilir. Bu, uygulamanızın güvenilirliğini artırır.
Action Odaklı Geliştirme: State değişiklikleri, anlamlı action nesneleri aracılığıyla tetiklenir (örneğin,
{ type: 'ARTTIR', payload: 1 }). Bu, hangi değişikliğin neden yapıldığını anlamayı kolaylaştırır.Performans Optimizasyonu Potansiyeli:
useReducerile birlikteuseCallbackveReact.memogibi performans optimizasyon hook'larını kullanarak gereksiz yeniden render'ları engelleyebilirsiniz.
useReducer'ı Aksiyonda Görelim: Bir "Adım Sayacı" Uygulaması
Karmaşık bir form yerine, birden fazla action tipi ve ilişkili state içeren basit bir adım sayacı (step counter) örneği ile başlayalım.
Reducer Fonksiyonu Tanımlama
Öncelikle state'i ve action'ları yönetecek reducer fonksiyonumuzu tanımlayalım:
const initialState = { count: 0, step: 1 };
function counterReducer(state, action) {
switch (action.type) {
case 'ARTTIR':
return { ...state, count: state.count + state.step };
case 'AZALT':
return { ...state, count: state.count - state.step };
case 'ADIMI_AYARLA':
return { ...state, step: action.payload };
case 'RESETLE':
return initialState;
default:
throw new Error('Geçersiz action tipi');
}
}Burada dikkat edilmesi gerekenler:
- Reducer fonksiyonu
stateveactionalır. - Her
casebloğu, mevcut state'i doğrudan değiştirmek yerine yeni bir state nesnesi döndürür (immutable güncelleme). action.payload, action ile birlikte taşınan ek veridir.- Varsayılan (
default) durumda bir hata fırlatmak, uygulamanın beklenmeyen action'ları ele almasına yardımcı olur.
Bileşeni Oluşturma
import React, { useReducer } from 'react';
// Reducer fonksiyonu ve initialState yukarıda tanımlandı.
const initialState = { count: 0, step: 1 };
function counterReducer(state, action) { /* ...yukarıdaki kod... */ }
function StepCounter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<h2>Adım Sayacı</h2>
<p>Mevcut Sayı: <strong>{state.count}</strong></p>
<p>Adım Değeri: <strong>{state.step}</strong></p>
<button onClick={() => dispatch({ type: 'AZALT' })}>- Azalt</button>
<button onClick={() => dispatch({ type: 'ARTTIR' })}>+ Arttır</button>
<br /><br />
<input
type="number"
value={state.step}
onChange={(e) => dispatch({ type: 'ADIMI_AYARLA', payload: parseInt(e.target.value) || 1 })}
/>
<button onClick={() => dispatch({ type: 'RESETLE' })}>Resetle</button>
</div>
);
}
export default StepCounter;Bu örnekte, count ve step değerlerini tek bir state nesnesinde topladık ve tüm güncelleme mantığını counterReducer fonksiyonuna taşıdık. Artık bileşenimiz sadece UI ile ilgileniyor, state'in nasıl değiştiği bilgisi reducer'da soyutlanmış durumda.
İleri Düzey useReducer Kullanım Senaryoları
1. Lazy Initialization (Tembel Başlatma)
Eğer başlangıç state'iniz pahalı bir hesaplama gerektiriyorsa (örneğin, localStorage'dan veri okumak), useReducer'ın üçüncü argümanı olan bir başlatıcı fonksiyonu kullanabilirsiniz. Bu fonksiyon sadece bileşen ilk render edildiğinde çalışır.
function init(initialCount) {
return { count: initialCount, step: 1 };
}
function StepCounterWithLazyInit({ initialCount = 0 }) {
const [state, dispatch] = useReducer(counterReducer, initialCount, init);
// ... geri kalan kod aynı ...
}2. Reducer'ı ve Dispatch'i Alt Bileşenlere Aktarmak
Eğer bir alt bileşenin state'i değiştirmesi gerekiyorsa, dispatch fonksiyonunu prop olarak aktarabilirsiniz. Alt bileşenler, üst bileşenin state'inin detaylarını bilmeden sadece action'ları tetikler.
function ControlButtons({ dispatch }) {
return (
<div>
<button onClick={() => dispatch({ type: 'AZALT' })}>- Azalt</button>
<button onClick={() => dispatch({ type: 'ARTTIR' })}>+ Arttır</button>
</div>
);
}
// Üst bileşende:
<ControlButtons dispatch={dispatch} />Bu yaklaşım, alt bileşenlerin daha soyut olmasını sağlar ve üst bileşenin state yapısına daha az bağımlı hale gelmelerine yardımcı olur.
3. useReducer ve useContext Birlikteliği
Karmaşık bir state'i, tek bir bileşende değil de, aynı ağaçtaki birden fazla bileşen arasında paylaşmanız gerekiyorsa, useReducer'ı useContext ile birleştirebilirsiniz. Bu, Redux'ın hafif bir yerel alternatifi olarak işlev görebilir ve prop drilling'i engeller.
import React, { createContext, useReducer, useContext } from 'react';
// 1. Context Oluştur
const CounterContext = createContext();
// 2. Reducer ve Initial State
const initialState = { count: 0 };
function counterReducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: throw new Error();
}
}
// 3. Provider Bileşeni
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// 4. State'e Erişen Bileşenler
function DisplayCount() {
const { state } = useContext(CounterContext);
return <p>Count: {state.count}</p>;
}
function CounterButtons() {
const { dispatch } = useContext(CounterContext);
return (
<div>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
// Uygulamanızda kullanım:
// <CounterProvider>
// <DisplayCount />
// <CounterButtons />
// </CounterProvider>useReducer vs useState: Ne Zaman Hangisini Kullanmalı?
İki hook da state yönetimi için kullanılırken, aralarındaki farklar ne zaman hangisini seçeceğimizi belirler:
useStateKullanım Durumları:- Basit ve bağımsız state parçacıkları (örneğin, bir bileşenin açık/kapalı durumu, bir input'un değeri).
- State güncelleme mantığı çok az olduğunda veya hiç olmadığında.
- State'in bir önceki durumuna bağımlılık olmadığında (örn. sadece bir değeri set etmek).
useReducerKullanım Durumları:- State mantığı karmaşık olduğunda ve birden fazla action tipi içerdiğinde.
- State birden fazla, birbiriyle ilişkili alt değere sahip olduğunda.
- State güncellemeleri, bir önceki state'e çok fazla bağımlı olduğunda.
- State mantığını bileşenin dışına çıkararak test edilebilirliği artırmak istediğinizde.
- Uygulamanız Redux'a benzer bir state yönetim modeli benimsiyorsa ancak global bir çözümün karmaşıklığını istemiyorsanız.
Genel bir kural olarak, eğer state'iniz bir obje veya dizi ise ve bu objenin/dizinin birden fazla özelliğini veya elemanını aynı anda, farklı aksiyonlarla güncelliyorsanız, useReducer genellikle daha temiz ve daha yönetilebilir bir çözüm sunar. 
En İyi Uygulamalar ve İpuçları
- Immutable State Güncellemeleri: Reducer içinde her zaman yeni state objeleri/dizileri döndürün. Mevcut state'i doğrudan mutate etmeyin. Spread operatörü (
...) veya Immer gibi kütüphaneler bu konuda size yardımcı olabilir. - Anlamlı Action Tipleri: Action'larınızın
typealanları, neyin değiştiğini açıkça ifade etmelidir (örn.'ADD_ITEM','UPDATE_USER_PROFILE'). - Action Creator'lar Kullanın: Özellikle action'lar karmaşık payload'lara sahipse, action creator fonksiyonları (action objelerini döndüren fonksiyonlar) kullanmak kodunuzu daha okunaklı hale getirir.
- Tek Sorumluluk Prensibi: Reducer'larınızı da tıpkı diğer kod parçaları gibi tek bir sorumluluğa sahip olacak şekilde tasarlayın. Çok büyük reducer'lar yerine, birden fazla, daha küçük ve özelleşmiş reducer'ı birleştirebilirsiniz (Redux'taki
combineReducersbenzeri). - Hata Yakalama: Reducer içinde beklenmeyen action tipleri için bir
defaultdurum ekleyin ve anlamlı bir hata fırlatın.
Sonuç
React'te useReducer hook'u, karmaşık yerel state yönetimi için useState'e göre çok daha güçlü ve esnek bir alternatiftir. State güncelleme mantığını bileşenden soyutlayarak daha temiz, daha öngörülebilir, test edilebilir ve bakımı kolay bileşenler oluşturmanızı sağlar. Özellikle birden fazla, birbiriyle ilişkili state parçacığını yönetmeniz gereken senaryolarda, useReducer, uygulamanızın kalitesini artırmak için vazgeçilmez bir araç haline gelebilir.
Her zaman olduğu gibi, doğru aracı doğru soruna uygulamak önemlidir. Projenizin ihtiyaçlarını iyi analiz ederek useState mi yoksa useReducer mı kullanacağınıza karar verebilirsiniz. Eğer aklınıza takılan sorular olursa veya bu konular hakkında daha fazla bilgi almak isterseniz, bana ismailyagci371@gmail.com adresinden veya sosyal medya kanallarımdan ulaşabilirsiniz. Sağlıklı ve başarılı kodlamalar dilerim!
Yorumlar
Yorum Gönder