Redux Thunk ile Asenkron Redux Eylemlerini Anlamak
Giriş: Redux ve Asenkronluğun Zorunluluğu
Modern web uygulamaları, kullanıcı deneyimini zenginleştirmek ve verimliliği artırmak için sürekli olarak sunucuyla iletişim kurar. Kullanıcı verilerini çekmek, form göndermek, dosya yüklemek veya anlık bildirimler almak gibi işlemlerin tamamı asenkron olarak gerçekleşir. Bu tür işlemler, uygulamanın ana iş parçacığını (main thread) bloke etmeden arka planda yürütülür, böylece kullanıcı arayüzü (UI) donmaz ve akıcı bir deneyim sunulur.
React uygulamalarında durum yönetimini merkezi ve öngörülebilir bir şekilde sağlamak için Redux gibi kütüphaneler yaygın olarak kullanılır. Redux, uygulamanın tüm durumunu tek bir yerde (store) tutan, bu durumu yalnızca saf (pure) fonksiyonlar (indirgeyiciler/reducers) aracılığıyla ve yalnızca eylemler (actions) ile değiştirilebilen bir mimari sunar. Bu prensipler, uygulamanın durumunu takip etmeyi, hataları ayıklamayı ve test etmeyi kolaylaştırır. Ancak Redux’ın bu temel prensipleri, asenkron işlemlerle doğal olarak çelişir.
Redux’ın indirgeyicileri, verilen bir durum ve eylem için her zaman aynı çıktıyı üretmeli ve herhangi bir yan etki (side effect) içermemelidir. Yan etkiler, bir fonksiyonun dış dünyayı etkilemesi veya dış dünyadan etkilenmesi anlamına gelir; örneğin bir API çağrısı yapmak, bir zamanlayıcı ayarlamak veya konsola çıktı yazmak gibi. Asenkron işlemler doğası gereği yan etkiler içerir ve bu nedenle doğrudan indirgeyiciler içinde ele alınamazlar. Eylemlerin ise sadece durumu tanımlayan düz JavaScript nesneleri olması beklenir. Peki, bir API çağrısı gibi zaman alıcı bir işlemi Redux akışına nasıl entegre edebiliriz? İşte bu noktada Redux middleware’leri ve özellikle Redux Thunk devreye girer. Redux Thunk, Redux’ın asenkron işlemleri yönetme yeteneğini artıran basit ama güçlü bir araçtır. Bu makalede, Redux Thunk’ın ne olduğunu, nasıl çalıştığını ve asenkron Redux eylemlerini anlamak ve uygulamak için neden bu kadar önemli olduğunu detaylı bir şekilde inceleyeceğiz.
Senkron Redux Eylemlerine Hızlı Bir Bakış
Redux’ın temelini oluşturan senkron eylem akışını anlamak, asenkron eylemlerin neden özel bir yaklaşıma ihtiyaç duyduğunu kavramak için kritik öneme sahiptir. Redux’ta, uygulamanın durumunu değiştirmek istediğinizde aşağıdaki adımlar izlenir:
1. Eylem Yaratıcısı (Action Creator): Durumda bir değişiklik niyetini ifade eden bir fonksiyon tanımlarsınız. Bu fonksiyon, bir “eylem” nesnesi döndürür.
function incrementCounter() {
return {
type: 'INCREMENT',
payload: 1
};
}
Burada incrementCounter bir eylem yaratıcısıdır.
2. Eylem (Action): Eylem yaratıcısının döndürdüğü, type özelliğine sahip düz bir JavaScript nesnesidir. type özelliği, hangi tür değişikliğin yapılacağını belirtir. Genellikle payload gibi ek veriler de içerebilir.
{ type: 'INCREMENT', payload: 1 }
Bu, yukarıdaki eylem yaratıcısının döndürdüğü eylem nesnesidir.
3. dispatch Fonksiyonu: Bir bileşenden veya başka bir yerden, bu eylem nesnesini Redux store’una göndermek için store.dispatch() fonksiyonunu çağırırsınız.
store.dispatch(incrementCounter());
Bu çağrı, eylemi Redux akışına sokar.
4. İndirgeyici (Reducer): Redux store’u, kendisine gönderilen eylemi alır ve bu eylemi tüm kayıtlı indirgeyicilere iletir. Her indirgeyici, uygulamanın belirli bir bölümünün durumunu yönetir. Bir indirgeyici, mevcut durumu ve eylemi girdi olarak alır ve eyleme göre yeni bir durum nesnesi döndürür. İndirgeyiciler saf fonksiyonlar olmalıdır; yani yan etki içermemeli, doğrudan durumu değiştirmemeli (immutable olmalı) ve her zaman aynı girdi için aynı çıktıyı üretmelidir.
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + action.payload };
case 'DECREMENT':
return { ...state, count: state.count - action.payload };
default:
return state;
}
}
5. Yeni Durum (New State): İndirgeyiciler yeni durumu döndürdüğünde, Redux store’u güncellenir. Bu güncelleme, Redux’a bağlı tüm bileşenlerin (örneğin React bileşenleri useSelector hook’u ile) yeniden render edilmesine neden olabilir, böylece UI yeni durumu yansıtır.
Bu akış tamamen senkrondur ve her adım anında gerçekleşir. Eylem nesnesi oluşturulur, gönderilir, indirgeyici tarafından işlenir ve durum güncellenir; bunların hepsi tek bir olay döngüsü içinde tamamlanır.
Redux’ta Asenkronluğun Zorlukları
Redux’ın temel prensipleri, uygulamanın durumunu öngörülebilir ve izlenebilir kılmak için tasarlanmıştır. Ancak bu prensipler, asenkron işlemlerle karşılaşıldığında doğal zorluklar yaratır:
1. İndirgeyicilerin Saflığı (Purity of Reducers): Redux’ın en temel kurallarından biri, indirgeyicilerin saf fonksiyonlar olmasıdır. Saf fonksiyonlar:
* Aynı girdilerle çağrıldığında her zaman aynı çıktıyı üretirler.
* Dış dünyayı etkileyen veya dış dünyadan etkilenen herhangi bir yan etki (side effect) içermezler.
* Girdilerini doğrudan değiştirmezler (immutable olmalıdırlar).
Bir API çağrısı yapmak, bir setTimeout kullanmak veya bir veritabanı sorgusu çalıştırmak gibi asenkron işlemlerin hepsi yan etkilerdir. Bu işlemlerin sonucu zamanla değişebilir, dış kaynaklara bağımlıdır ve genellikle tahmin edilemez bir süre alır. Bu nedenle, bir API çağrısını doğrudan bir indirgeyici içinde yapmak, indirgeyicinin saflık ilkesini ihlal eder ve Redux’ın temel mimarisini bozar.
2. Eylemlerin Düz Nesneler Olması Gerekliliği: Redux’ta, dispatch fonksiyonuna gönderilen eylemlerin basit, düz JavaScript nesneleri olması beklenir. Bu nesneler, type özelliği aracılığıyla ne tür bir değişikliğin yapılacağını belirtir ve isteğe bağlı olarak payload gibi ek veriler içerebilir. Eylemlerin düz nesneler olması, seri hale getirilmelerini (serialize) ve zaman yolculuğu hata ayıklama (time-travel debugging) gibi Redux DevTools özelliklerinin düzgün çalışmasını sağlar.
Bir API çağrısı gibi asenkron bir işlemi temsil eden bir eylemin düz bir nesne olması mümkün değildir. Bir API çağrısı, isteği başlatma, veriyi bekleme ve başarılı veya başarısız olma gibi birden fazla aşamadan geçer. Bu aşamaların her biri farklı bir durumu veya olayı temsil eder ve tek bir düz nesneyle ifade edilemez.
Problem Özeti: Bir kullanıcı listesini sunucudan çekmek istediğimizi varsayalım. Bu işlem şu adımları içerecektir:
* Kullanıcı verisi çekme isteğini başlatma.
* API çağrısının sonucunu bekleme.
* API çağrısı başarılı olursa, gelen veriyi Redux store’a kaydetme.
* API çağrısı başarısız olursa, hatayı Redux store’a kaydetme.
Bu adımların her biri, farklı bir Redux eylemi gerektirebilir (FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE). Ancak bu eylemleri ne zaman dispatch edeceğiz? API çağrısının kendisi nerede yapılacak? Doğrudan dispatch çağrısından önce mi? Yoksa bir indirgeyici içinde mi? Bu sorular, Redux’ın asenkron işlemler için doğrudan bir mekanizma sunmadığını ve bir köprüye ihtiyaç duyulduğunu açıkça gösterir.
Middleware Kavramı: Redux Akışındaki Köprü
Redux’ta asenkron işlemlerin ve diğer yan etkilerin yönetilmesi için kullanılan temel mekanizma “middleware”dir. Middleware, Redux’ın dispatch fonksiyonuna gönderilen eylemler ile bu eylemlerin indirgeyicilere ulaşması arasına yerleşen bir katmandır. Bir middleware, bir eylemi yakalayabilir, üzerinde değişiklik yapabilir, bir şeyleri loglayabilir, API çağrıları başlatabilir, başka eylemler dispatch edebilir ve hatta eylemin indirgeyicilere ulaşmasını engelleyebilir.
Middleware Nasıl Çalışır?
Redux’ın dispatch fonksiyonu, bir eylemi doğrudan indirgeyicilere göndermeden önce middleware zincirinden geçirir. Her middleware, bir eylemi işleme yeteneğine sahiptir ve bir sonraki middleware’e (veya zincirin sonundaysa indirgeyicilere) eylemi iletmek için bir next fonksiyonu sağlar.
Bir middleware’in yapısı genellikle şu şekildedir:
const myMiddleware = store => next => action => {
// Eylem indirgeyicilere ulaşmadan önce burada bir şeyler yapabiliriz
console.log('Dispatch edilen eylem:', action.type);
// Eylemi bir sonraki middleware'e veya indirgeyicilere ilet
const result = next(action);
// Eylem işlendikten sonra burada bir şeyler yapabiliriz
console.log('Yeni durum:', store.getState());
return result;
};
* store: getState() ve dispatch() gibi store metotlarına erişim sağlar.
* next: Zincirdeki bir sonraki middleware’i (veya en sondaysa orijinal dispatch fonksiyonunu) temsil eden fonksiyondur. Eylemi next(action) ile çağırarak zincirde ilerletiriz.
* action: dispatch edilen mevcut eylem nesnesidir.
Bu yapı sayesinde middleware’ler, Redux’ın tek yönlü veri akışını bozmadan, eylemleri yakalayabilir ve yan etkileri yönetebilir. Asenkron işlemler için Redux Thunk gibi middleware’ler, eylem yaratıcılarının düz nesneler yerine fonksiyonlar döndürmesine izin vererek bu köprü görevini üstlenir.
Redux Thunk’ı Tanıyalım
Redux Thunk, Redux için en popüler ve en basit middleware’lerden biridir. Temel amacı, Redux’ın asenkron işlemleri doğal bir şekilde yönetmesini sağlamaktır. Bunu, eylem yaratıcılarının düz JavaScript nesneleri döndürmek yerine fonksiyonlar döndürmesine izin vererek yapar.
Thunk Nedir?
Bilgisayar biliminde “thunk”, bir hesaplamayı erteleyen bir fonksiyonu ifade eder. Yani, bir fonksiyonun değerini hemen hesaplamak yerine, bu değeri hesaplayacak bir fonksiyon döndürürsünüz. Değer, bu döndürülen fonksiyon çağrıldığında hesaplanır. Redux Thunk bağlamında, bu, bir eylemi hemen dispatch etmek yerine, asenkron bir işlem tamamlandığında dispatch edilecek bir fonksiyon döndürmek anlamına gelir.
Redux Thunk Nasıl Çalışır?
Redux Thunk middleware’i, dispatch edilen her eylemi kontrol eder:
1. Düz Nesne İse: Eğer dispatch edilen şey düz bir JavaScript nesnesiyse (yani senkron bir eylemse), Thunk middleware’i onu es geçer ve bir sonraki middleware’e veya doğrudan indirgeyicilere iletir.
2. Fonksiyon İse: Eğer dispatch edilen şey bir fonksiyonsa (yani bir thunk eylemiyse), Thunk middleware’i bu fonksiyonu çağırır. Bu fonksiyon, iki argüman alır:
* dispatch: Redux store’unun dispatch fonksiyonu. Bu sayede thunk içinde başka eylemler dispatch edilebilir (örneğin, API isteğinin başladığını, başarılı olduğunu veya başarısız olduğunu belirten eylemler).
* getState: Redux store’unun getState fonksiyonu. Bu, thunk içinde mevcut Redux durumuna erişim sağlar. Böylece, mevcut duruma göre koşullu mantık yürütülebilir.
Bu mekanizma, thunk fonksiyonunun içinde yan etkileri (API çağrıları, zamanlayıcılar vb.) güvenle gerçekleştirmesine ve bu yan etkilerin sonucuna göre Redux store’unu güncellemek için birden fazla eylemi dispatch etmesine olanak tanır.
Neden Güçlü?
* Yan Etkileri Yönetme: Thunk içinde API çağrıları gibi yan etkileri güvenle gerçekleştirebilirsiniz.
* Birden Fazla dispatch: Tek bir asenkron işlem sırasında birden fazla eylem (REQUEST, SUCCESS, FAILURE gibi) dispatch edebilirsiniz.
* Koşullu Mantık: getState argümanını kullanarak mevcut duruma göre koşullu işlemler yapabilir veya belirli bir verinin zaten store’da olup olmadığını kontrol edebilirsiniz.
* Basitlik: Diğer asenkron middleware’lerine kıyasla (örneğin Redux Saga veya Redux Observable) öğrenme eğrisi çok daha düşüktür. JavaScript’in temel asenkron özelliklerini (Promise’ler, async/await) doğrudan kullanır.
Redux Thunk Uygulaması: Adım Adım
Redux Thunk’ı bir Redux projesine entegre etmek ve kullanmak oldukça basittir. İşte adım adım bir rehber:
1. Kurulum
Öncelikle Redux Thunk paketini projenize kurmanız gerekir:
npm install redux-thunk
veya
yarn add redux-thunk
2. Middleware’i Redux Store’una Uygulama
Redux Thunk’ı kullanabilmek için, onu Redux store’unuza bir middleware olarak uygulamanız gerekir. Bu işlem genellikle createStore (geleneksel Redux) veya configureStore (Redux Toolkit ile önerilen) fonksiyonları aracılığıyla yapılır.
Geleneksel Redux ile:
import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk'; // ES6 modülünden thunk'ı içe aktarın
import rootReducer from './reducers'; // Kök indirgeyiciniz
const store = createStore(
rootReducer,
applyMiddleware(thunk) // Redux Thunk'ı middleware olarak uygulayın
);
export default store;
Redux Toolkit ile (Önerilen):
Redux Toolkit, Redux uygulamalarını daha kolay ve verimli hale getirmek için tasarlanmıştır. configureStore fonksiyonu, varsayılan olarak redux-thunk ve diğer bazı faydalı middleware’leri içerir, bu yüzden genellikle manuel olarak eklemeniz gerekmez.
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers'; // Kök indirgeyiciniz
const store = configureStore({
reducer: rootReducer,
// middleware'i manuel olarak eklemeniz gerekmez, configureStore varsayılan olarak thunk'ı içerir.
// Ancak, özel bir middleware eklemek veya varsayılanları değiştirmek isterseniz:
// middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myCustomMiddleware),
});
export default store;
3. Örnek Senaryo: Kullanıcı Verisi Çekme (API Çağrısı)
Şimdi, bir API’den kullanıcı verisi çeken asenkron bir Redux eylemi oluşturalım. Bu işlem genellikle üç farklı durumu içerir: isteğin başlaması, isteğin başarıyla tamamlanması ve isteğin başarısız olması. Bu durumların her birini ele almak için farklı eylem tipleri tanımlayacağız.
a. Eylem Tipleri (Action Types)
// actions/userTypes.js
export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
b. İndirgeyici (Reducer)
Bu eylem tiplerini ele alacak bir indirgeyiciye ihtiyacımız var. Bu indirgeyici, kullanıcı verilerini, yükleme durumunu ve hata mesajlarını store’da tutacaktır.
// reducers/userReducer.js
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE,
} from '../actions/userTypes';
const initialState = {
loading: false,
users: [],
error: '',
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return {
...state,
loading: true,
};
case FETCH_USERS_SUCCESS:
return {
...state,
loading: false,
users: action.payload,
error: '',
};
case FETCH_USERS_FAILURE:
return {
...state,
loading: false,
users: [],
error: action.payload,
};
default:
return state;
}
};
export default userReducer;
c. Asenkron Eylem Yaratıcısı (Thunk Action Creator)
Şimdi asıl Redux Thunk fonksiyonumuzu yazalım. Bu fonksiyon, bir API çağrısı yapacak ve duruma göre ilgili eylemleri dispatch edecektir.
// actions/userActions.js
import axios from 'axios'; // API çağrıları için axios kullanacağız
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE,
} from './userTypes';
// Normal eylem yaratıcıları (düz nesneler döndürür)
export const fetchUsersRequest = () => ({
type: FETCH_USERS_REQUEST,
});
export const fetchUsersSuccess = (users) => ({
type: FETCH_USERS_SUCCESS,
payload: users,
});
export const fetchUsersFailure = (error) => ({
type: FETCH_USERS_FAILURE,
payload: error,
});
// THUNK EYLEM YARATICISI
export const fetchUsers = () => {
return async (dispatch) => { // Redux Thunk sayesinde bu fonksiyonu dispatch edebiliriz
dispatch(fetchUsersRequest()); // İstek başladığında yükleme durumunu ayarla
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
const users = response.data;
dispatch(fetchUsersSuccess(users)); // Başarılı olduğunda kullanıcıları store'a kaydet
} catch (error) {
dispatch(fetchUsersFailure(error.message)); // Hata olduğunda hata mesajını kaydet
}
};
};
Açıklama:
fetchUsers fonksiyonu, bir başka fonksiyon* döndürüyor. İşte bu döndürülen fonksiyon bir “thunk”tır.
* Bu iç fonksiyon dispatch ve getState argümanlarını alır (bu örnekte getState kullanmadık, ancak ihtiyacımız olsaydı erişebilirdik).
* İç fonksiyonun içinde, önce fetchUsersRequest() eylemini dispatch ederek yükleme durumunu true yaparız.
* Ardından axios.get() ile asenkron bir API çağrısı yaparız. async/await kullanarak kodu daha okunabilir hale getirdik.
* API çağrısı başarılı olursa, fetchUsersSuccess() eylemini dispatch ederek gelen kullanıcı verilerini store’a kaydederiz.
* API çağrısı başarısız olursa, fetchUsersFailure() eylemini dispatch ederek hata mesajını store’a kaydederiz.
4. Bileşen (Component) İçinden Kullanım
Son olarak, bu thunk eylemini bir React bileşeni içinden nasıl çağıracağımızı görelim. react-redux kütüphanesinin useDispatch ve useSelector hook’larını kullanacağız.
// components/UserList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from '../actions/userActions';
function UserList() {
const dispatch = useDispatch();
const userData = useSelector((state) => state.user); // userReducer'ın yönettiği state'i al
useEffect(() => {
dispatch(fetchUsers()); // Bileşen yüklendiğinde kullanıcıları çekmek için thunk'ı dispatch et
}, [dispatch]); // dispatch fonksiyonu değişmediği için tek seferlik çalışır
if (userData.loading) {
return Kullanıcılar yükleniyor...
;
}
if (userData.error) {
return Hata: {userData.error}
;
}
return (
Kullanıcı Listesi
{userData.users.map((user) => (
- {user.name}
))}
);
}
export default UserList;
Bu örnek, Redux Thunk’ın asenkron bir API çağrısını Redux akışına nasıl sorunsuz bir şekilde entegre ettiğini göstermektedir. Kullanıcı arayüzü, yükleme, başarı ve hata durumlarını store’daki duruma göre dinamik olarak günceller.
İleri Thunk Desenleri ve En İyi Uygulamalar
Redux Thunk’ın temel kullanımı basit olsa da, daha karmaşık senaryolar için bazı ileri desenler ve en iyi uygulamalar mevcuttur.
1. getState Kullanımı ile Koşullu Mantık
Thunk fonksiyonları, dispatch ile birlikte getState fonksiyonunu da argüman olarak alır. Bu, mevcut Redux durumuna erişmenizi ve buna göre koşullu mantık yürütmenizi sağlar.
Örnek: Veri Tekrar Çekmeyi Engelleme
Diyelim ki bir kullanıcının verilerini çekmek istiyorsunuz, ancak bu veriler zaten store’da mevcutsa API çağrısı yapmaktan kaçınmak istiyorsunuz.
export const fetchUserIfNeeded = (userId) => {
return async (dispatch, getState) => {
const state = getState();
const user = state.users.data[userId]; // Kullanıcının store'da olup olmadığını kontrol et
if (user && !state.users.loading) { // Kullanıcı varsa ve yüklenmiyorsa tekrar çekme
return;
}
// Eğer kullanıcı yoksa veya yükleniyorsa, çekme işlemini başlat
dispatch(fetchUserRequest());
try {
const response = await axios.get(/api/users/${userId});
dispatch(fetchUserSuccess(response.data));
} catch (error) {
dispatch(fetchUserFailure(error.message));
}
};
};
Bu desen, gereksiz ağ isteklerini önleyerek uygulamanızın performansını artırabilir.
2. Thunk’ları Zincirleme (Chaining Thunks)
Bir thunk içinde başka bir thunk’ı dispatch edebilirsiniz. Bu, daha büyük ve karmaşık asenkron iş akışlarını daha küçük, yönetilebilir parçalara bölmek için faydalı olabilir. Ancak, bu yaklaşım dikkatli kullanılmalı, aksi takdirde kod karmaşıklığı artabilir.
Örnek: Kullanıcı Oluşturma ve Ardından Tüm Kullanıcıları Yeniden Çekme
export const createUser = (userData) => {
return async (dispatch) => {
dispatch(createUserRequest());
try {
await axios.post('/api/users', userData);
dispatch(createUserSuccess());
dispatch(fetchUsers()); // Yeni kullanıcı oluşturulduktan sonra tüm kullanıcı listesini yeniden çek
} catch (error) {
dispatch(createUserFailure(error.message));
}
};
};
Bu desen, bir işlemin tamamlanmasının ardından başka bir işlemin tetiklenmesini sağlar.
3. Kod Organizasyonu
Büyük uygulamalarda, eylem tiplerini, eylem yaratıcılarını ve thunk’ları mantıksal olarak düzenlemek önemlidir. Genellikle her özellik alanı (feature) için ayrı bir klasör oluşturulur ve içinde actions.js, reducers.js, types.js gibi dosyalar bulunur.
src/
├── features/
│ └── users/
│ ├── actions.js // Thunk'lar ve normal eylem yaratıcıları
│ ├── reducers.js // Kullanıcı indirgeyicisi
│ └── types.js // Eylem tipleri
├── store/
│ └── index.js // Store konfigürasyonu
└── App.js
Redux Toolkit ile bu organizasyon genellikle createSlice kullanılarak daha da basitleştirilir, çünkü dilimler (slices) eylem tiplerini, eylem yaratıcılarını ve indirgeyicileri tek bir yerde birleştirir.
4. Thunk’ları Test Etme
Thunk’ları test etmek, içerdikleri asenkron mantık nedeniyle normal eylem yaratıcılarından biraz daha karmaşık olabilir. Ancak, dispatch ve API çağrılarını mock’layarak kolayca test edilebilirler.
// userActions.test.js
import { fetchUsers, fetchUsersRequest, fetchUsersSuccess, fetchUsersFailure } from './userActions';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; // axios isteklerini mocklamak için
describe('fetchUsers thunk', () => {
let store;
let mock;
beforeEach(() => {
// store'u mock'la, sadece dispatch fonksiyonu önemlidir
store = {
dispatch: jest.fn(), // dispatch'i bir mock fonksiyonu yap
getState: jest.fn(), // getState'i de mock'layabiliriz
};
mock = new MockAdapter(axios); // axios'u mock'la
});
afterEach(() => {
mock.restore(); // Her testten sonra mock'u sıfırla
});
it('başarılı bir API çağrısı durumunda doğru eylemleri dispatch etmeli', async () => {
const users = [{ id: 1, name: 'Test User' }];
mock.onGet('https://jsonplaceholder.typicode.com/users').reply(200, users);
await fetchUsers()(store.dispatch, store.getState); // Thunk'ı çağır
expect(store.dispatch).toHaveBeenCalledTimes(2); // REQUEST ve SUCCESS olmak üzere 2 kez dispatch edilmeli
expect(store.dispatch).toHaveBeenNthCalledWith(1, fetchUsersRequest());
expect(store.dispatch).toHaveBeenNthCalledWith(2, fetchUsersSuccess(users));
});
it('başarısız bir API çağrısı durumunda hata eylemini dispatch etmeli', async () => {
const errorMessage = 'Network Error';
mock.onGet('https://jsonplaceholder.typicode.com/users').networkErrorOnce();
await fetchUsers()(store.dispatch, store.getState);
expect(store.dispatch).toHaveBeenCalledTimes(2);
expect(store.dispatch).toHaveBeenNthCalledWith(1, fetchUsersRequest());
expect(store.dispatch).toHaveBeenNthCalledWith(2, fetchUsersFailure(errorMessage));
});
});
Bu testler, thunk’ın beklenen eylemleri doğru zamanlarda ve doğru verilerle dispatch ettiğinden emin olmanızı sağlar.
Redux Thunk Alternatifleri (Kısa Bir Bakış)
Redux ekosisteminde asenkron işlemleri yönetmek için Redux Thunk’tan başka güçlü middleware’ler de bulunmaktadır. En popüler alternatifler Redux Saga ve Redux Observable’dır.
* Redux Saga: Generator fonksiyonlarını (ES6) kullanarak yan etkileri yöneten bir middleware’dir. Saga’lar, daha bildirimsel bir yaklaşımla karmaşık asenkron iş akışlarını tanımlamanıza olanak tanır. İşlemleri iptal etme, eşzamanlılık kontrolü ve daha karmaşık durumlar için güçlü araçlar sunar. Öğrenme eğrisi Thunk’a göre daha diktir, ancak büyük ve karmaşık uygulamalarda daha iyi bir yapılandırma ve test edilebilirlik sağlayabilir.
* Redux Observable: RxJS (Reactive Extensions for JavaScript) kütüphanesini temel alan bir middleware’dir. Akış tabanlı (stream-based) bir yaklaşımla asenkron işlemleri ve yan etkileri yönetir. RxJS’in güçlü operatörleri sayesinde, olayların zaman içindeki akışını manipüle etmek ve dönüştürmek için çok esnek bir yol sunar. Reaktif programlamaya aşina olanlar için güçlü bir araçtır, ancak RxJS’in öğrenilmesi de ayrı bir çaba gerektirir.
Neden Redux Thunk Hala Popüler?
Redux Thunk, basitliği, hafifliği ve doğrudan JavaScript’in yerleşik asenkron özelliklerini (Promise’ler, async/await) kullanması nedeniyle birçok proje için tercih edilen seçenektir. Küçük ve orta ölçekli uygulamalar için genellikle fazlasıyla yeterlidir. Ek bir kütüphane veya paradigma (generator’lar, reaktif programlama) öğrenme ihtiyacını ortadan kaldırır, bu da yeni başlayanlar veya daha az karmaşık asenkron ihtiyaçları olan projeler için idealdir.
Redux Thunk’ın Artıları ve Eksileri
Her araç gibi, Redux Thunk’ın da kendi avantajları ve dezavantajları vardır.
Artıları:
1. Basit ve Hafif: Redux Thunk’ın kod tabanı oldukça küçüktür ve kavramsal olarak anlaşılması kolaydır. Redux’a yeni başlayanlar için ideal bir başlangıç noktasıdır.
2. Düşük Öğrenme Eğrisi: JavaScript’in mevcut asenkron yapılarını (Promise’ler, async/await) doğrudan kullanır. Ek bir paradigma veya özel bir syntax öğrenmenize gerek kalmaz.
3. Doğrudan dispatch ve getState Erişimi: Thunk fonksiyonu içinde store’un dispatch ve getState metotlarına doğrudan erişim, büyük esneklik sağlar.
4. Esneklik: Asenkron işlemleri, koşullu mantığı ve birden fazla eylem dispatch etmeyi kolayca yönetebilir.
5. Yaygın Kullanım: Redux Toolkit tarafından varsayılan olarak dahil edilmesi ve geniş topluluk desteği sayesinde yaygın olarak kullanılır.
Eksileri:
1. Karmaşık Mantık Birikimi: Çok karmaşık asenkron akışlar veya birden fazla bağımlı API çağrısı içeren senaryolarda, thunk fonksiyonlarının içinde çok fazla mantık birikebilir. Bu, thunk’ların okunabilirliğini ve bakımını zorlaştırabilir (“callback hell” benzeri bir durum).
2. İşlem İptali Zorluğu: Devam eden bir asenkron işlemi (örneğin bir API çağrısı) iptal etmek, Thunk ile doğrudan desteklenmez ve manuel olarak yönetilmesi gerekir. Redux Saga veya Redux Observable gibi alternatifler, bu tür senaryolar için daha güçlü yerleşik mekanizmalara sahiptir.
3. Test Edilebilirlik (Bazen Daha Zor): Thunk’lar genellikle yan etkiler içerdiği için, test yazarken API çağrılarını ve dispatch fonksiyonunu mock’lamak gerekir. Bu, bazı durumlarda Redux Saga’nın daha bildirimsel test yaklaşımlarına göre biraz daha zorlayıcı olabilir.
4. Gelişmiş Özellik Eksikliği: Eşzamanlılık kontrolü, debouncing/throttling gibi gelişmiş yan etki yönetimi desenleri için Thunk’ın üzerine manuel olarak kod yazmak gerekir.
Sonuç
Redux Thunk, Redux uygulamalarında asenkron işlemleri yönetmek için vazgeçilmez bir araçtır. Redux’ın indirgeyicilerin saflığı ve eylemlerin düz nesneler olması gibi temel prensipleriyle asenkronluğun doğasındaki yan etkiler arasındaki boşluğu doldurur. Eylem yaratıcılarının düz nesneler yerine fonksiyonlar döndürmesine izin vererek, bu fonksiyonların içinde API çağrıları yapma, birden fazla eylem dispatch etme ve mevcut Redux durumuna erişerek koşullu mantık yürütme yeteneği sağlar.
Kurulumu ve kullanımı kolay olması, düşük öğrenme eğrisi sunması ve JavaScript’in yerleşik asenkron mekanizmalarını kullanması nedeniyle Redux Thunk, özellikle küçük ve orta ölçekli projeler için popüler bir tercih olmaya devam etmektedir. Redux Toolkit’in varsayılan olarak Thunk’ı içermesi, onun Redux ekosistemindeki merkezi rolünü daha da pekiştirmektedir.
Ancak, çok karmaşık asenkron iş akışları, iptal edilebilir işlemler veya yüksek düzeyde eşzamanlılık gerektiren durumlarla karşılaşıldığında, Redux Saga veya Redux Observable gibi daha güçlü ve bildirimsel alternatifler değerlendirilebilir. Yine de, çoğu Redux uygulaması için Redux Thunk, asenkron eylemleri temiz, öngörülebilir ve yönetilebilir bir şekilde ele almak için yeterli ve etkili bir çözümdür. Redux Thunk’ı anlamak ve ustaca kullanmak, modern Redux tabanlı uygulamalar geliştiren her geliştirici için temel bir beceridir.