Redux-Thunk ile asenkron işlemler ve middleware kullanımı
Eğer Redux'a giriş yaptıysanız redux-thunk'ı duymuşsunuzdur. Thunk; saf olmayan fonksiyonlarımızı, saf Redux actionlarımızla kaynaştıran bir middleware'dir. Redux reducer'larımızın her daim saf olması, uygulamamızın sağlıklı işlemesi açısından çok önemlidir. Peki nedir bu "saf" fonksiyonlar ve bu saflık neden önemlidir?
Kısaca saf fonksiyon, aynı girdilere her zaman aynı çıktıyı üreten fonksiyonlara denir.
function topla(a, b) {
return a+b;
}
Örneğin topla fonksiyonu bir saf fonksiyondur 2+3 zamandan, mekandan ve veritabanındaki herhangi bir veriden bağımsız olarak 5'tir.
function rastgeleTopla(a){
return Math.random() + a;
}
Fakat yukarıdaki bu fonksiyon, her çağırdığımızda farklı sonuçlar üretebileceği için saf değildir. Benzer şekilde tarihe bağlı fonksiyonlar, veritabanından aldığı veriye göre sonuç üreten fonksiyonlar da saf değildir. Ancak hesap makinası dışında bir uygulama geliştiriyor isek elbette saf olmayan fonksiyonlarımız olacak.
Aslında asenkron fonksiyonlarımızı yazmak için herhangi bir middleware yapısına ihtiyacımız yok. Ancak Redux yapısını 'saf' tutabilmek için asenkron fonksiyonları veya genel olarak yan etkiye sahip fonksiyonları redux-thunk (veya saga) gibi middleware içerisinde işlememiz gerekir.
Yapınızı saf tuttuğunuzda öncelikle kodunuzun okunması çok daha kolay olacaktır. Asenkron cevapların olduğu fonksiyonlar için unit test yazmak oldukça zordur. Saf olmayan reducer kullandığınızda buna bağlı olan değişkenler değiştiğinde DOM üzerinde beklediğiniz güncellemeyi göremeyebilirsiniz. Ayrıca hot-reloading, time-travel gibi yazılım geliştirirken oldukça yararlı olan fonksiyonları devre dışı kalacaktır.
Eğer NodeJS ile Express.JS üzerinde uygulamalar geliştirdiyseniz middleware kavramı sizin için yeni değildir. Middleware Redux'ta da benzer şekilde işliyor. Action yerine function dispatch ettiğinizde middleware devreye giriyor. Redux-thunk'ın kaynak koduna bakarsak bunun nereden geldiğini görebiliriz.
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
Evet redux-thunk'ın tüm olayı bu 15 satırlık koddan ibaret. Redux'ta dispatch edilen tüm işlemler öncelikle middleware yapısına uğrar. Thunk da bir middleware olduğundan öncelikle gelen action'a bakıyor. Function tipinde bir işlem dispatch edildiğinde devreye giriyor. Thunk'ın ana sayfasında hiç anlayamadığım şu cümleyi ancak kaynak koduna baktıktan sonra anlayabildim:
Redux Thunk middleware allows you to write action creators that return a function instead of an action.
Redux thunk middleware yapısı action yerine fonksiyon dönebilmenizi sağlar.
İyi de neden fonksiyon dönmemi sağlıyor? Amacım neden fonksiyon döndürmek olsun? Bunun hakkında pek bir şey söylemediğinden kafa karıştırıcı. Aslında fonksiyon dönmek zorunda olmayabilirdik. Middleware o şekilde dizayn edildiği için fonksiyon dönmek zorundayız. Yani tanımı aslında çok kafa karıştırıcı. Ben başka bir middleware yazıp örneğin adı hasan ile başlayan tüm fonksiyonların middleware'i tetiklemesini sağlayabilirdim.
if (action.name.startsWith("hasan")) {
return action(dispatch, getState, extraArgument);
}
Bu durumda redux-thunk adı hasan ile başlayan fonksiyonlar dönmenizi sağlayan bir yapıdır mı diyecektim. Neyse absürd bir örnek oldu ancak demek istediğim thunk'ta action yerine fonksiyon dönmemizin tek amacı middleware'e bir nevi sinyal göndermek.
Middleware devreye girdiğinde, Redux kontrolü bir süreliğine middleware'e verecek; middleware asenkron işlemini tamamladığında Redux da tüm veri ağacını güncelleyip React'e gönderecek. Bu şekilde Redux fonksiyonlarımız saf kalacak ve React uygulamamız stabil çalışacak.
Şimdi de ufak bir örnekle thunk'ı nasıl kullanacağımıza bakalım:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
Thunk middleware'i reducerlarımızla birlikte Redux'a ilettik.
export const asenkronIslem = params => {
return (dispatch, getState) => {
fetch('http://example.com').then(
response => {
dispatch(senkronIslem(response));
}
);
};
};
Burada senkron işlem, asenkron isteğin tamamlanmasına bağlı olduğundan öncelikle asenkronIslem'i dispatch etmemiz gerekiyor. Asenkron işlem tamamlandığında gelen cevaba göre istediğimiz senkron işlemi devreye sokabiliriz.