What are useCallBack, useMemo and useEffect? How to and When use? What are the difference?
Reactjs hooklarından useCallback, useEffect ve useMemo hookları zaman zaman birbiriyle karışan hookslardır. Bu üç hooksun nasıl çalıştığını ve ne zaman kullanılacağını inceleyeceğiz.
useEffect
useEffect Component içindeki state, props gibi varlıklar render olduktan sonra tetiklenen bir fonksiyondur. Bu tetiklenmeyi bir dizi içindeki verilen varlıklara göre yapar.
En basit kullanımı aşağıdaki şekildedir. Counter state’i her değişim sonrası tetiklenecektir. Önceki değeriyle yeni değeri aynı ise render olmayacaktır.
1 2 3 4 |
const [counter, setCounter] = useState(0); useEffect(() => { console.log("handle change") },[counter]) |
Depedency array içine fonksiyon eklenmesi durumunda o fonksiyonun her çağrılmasında tetiklenir.
1 2 3 4 5 6 7 |
const [counter, setCounter] = useState(0); const handleChange = () => { setCounter(counter + 1); } useEffect(() => { console.log("handle change") },[handleChange]) |
Array boş olması durumunda bağlı olduğu component render olduktan sonra tetiklenir. Önce return içindekiler render olur daha sonra useEffect.
1 2 3 |
useEffect(() => { console.log("handle change") },[]) |
Yukarıda verdiğimiz örneklerde hep mount işlemleri yaptık yani değişimi yakala ve çalıştır. Peki çalıştırma bittikten sonra tetiklemek istersek ne yapmalıyız (unmount)?
1 2 3 4 5 6 |
useEffect(() => { console.log("handle change", counter);//new value return () => { console.log("un mount", counter);//prev value }; }, [counter]); |
return () => counter değeri güncellemeden önce handle eden kısım. Genellikle window.addEventListener ve timerlarda uygulanan bir davranıştır.
1 2 3 4 5 6 |
useEffect(() => { window.addEventListener("keyup", keyUpHandle); return () => { window.removeEventListener("keyup",keyUpHandle) }; }, [counter]); |
Bu örnekte counter her değiştiğinde window’a keyup eventi için bir fonksiyon ekliyoruz. Eğer unmount ile remove yapmasaydık her useEffect içine girişte keyup için event ekleyecekti. Örneğin 5 kez counter değiştiyse keyup için 5 tane handler olacaktı. Böylece bir keyup yaptığımızda keyupHandler 5 kez tetiklenecektir.
useCallback
Ana componenet her render olduğunda içindeki fonksiyonların bir instance yeniden oluşturulur, useCallback gereksiz yere oluşturulmasını engeller ve fonskiyonu memoize eder. useEffect gibi bir bağımlı dizisine sahiptir, objeler belirlenip sadece bu objeler değiştiğinden yeniden oluşturulacak şekilde çalışır.
1 2 3 4 5 6 7 8 9 10 |
const handleSearch = (value) => { setSearchValue(value); setList(search(value)); }; const search = useCallback((term) => { console.log("count",counter) return term ? list.filter((p) => p.toLowerCase().includes(term.toLowerCase())) : list; },[]); |
Yukarıdaki örnekte uygulama ilk render olduğunda fonksiyonu memoize eder, yani list ve counter değerleri değişse bile eski değerleri ile arama yapar. Eğer memoize etmeseydik her seferinde function yeniden oluşturacaktı. Ama bu yukarıdaki örnekte listeye bir değer eklense bile fonksiyonda hâlâ eski liste göre arama yapacaktır bunu düzeltmek için bağımlılık dizesine listi eklemeliyiz.
1 2 3 4 5 |
const search = useCallback((term) => { return term ? list.filter((p) => p.toLowerCase().includes(term.toLowerCase())) : list; },[list]); |
Eğer ki büyük bir list var ise yukarıdaki bir mantıkta fonksiyonu memoize etmek gayet mantıklı, eğer maliyeti yüksek olmayan bir fonksiyonunuz var ise gereksiz yük olur. memoize etmemenin de kendine göre bir maliyeti var ve bazen organizasyonu zor olabiliyor.
useMemo
useMemo’nun çok detaylı açıklanmış bir örnek yazısını bakabilirsiniz. What are React Memo and useMemo? What situations is it used?
useMemo hooksu useCallback fonksiyonu gibi bir memoize işlemi yapar ama sadece fonksiyonu memoize etmez, bir girdiye göre çıkan sonucu hesaplar ve onu memoize eder.
Örneğin faktöriyel hesaplayan bir metodumuz var. Girdimiz 5 ise 120 çıkış olur ve bunu hafızaya atar. Siz tekrar 5’in faktöriyelini hesapladığınız zaman yeniden hesaplamaz ve direkt 120’yi döner. Bu örnekler çoğaltılabilir örneğin +4k sabit bir liste üzerinde sıralama yapıyorsunuz. Bu sıralama’da iki tür olabilir artan ya da azalan. Listi cache’e atar ve bağımlılıkları değişmedikçe yeniden hesaplama yapmaz.
1 2 3 4 5 6 7 8 9 10 |
//UNNECESSARY USE useEffect(() => { setDefList(sortedList); },[isIncrement]) const sortedList = useMemo(() => { if(isIncrement){ return defList.sort((a,b) => {return a > b ? 0 : -1}); } return defList.sort((a,b) => {return a > b ? -1 : 0}); },[defList,isIncrement]) |
Yukarıdaki örnek gereksiz bir kullanımdır. Zaten isIncrement değeri aynı kaldıkça useEffect tetiklenmeyecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const sortedList = useMemo(() => { if(isIncrement){ return defList.sort((a,b) => {return a > b ? 0 : -1}); } return defList.sort((a,b) => {return a > b ? -1 : 0}); },[defList,isIncrement]) .... .... .... return( <div> <button onClick={() => handleSort(true)}>Sort +</button> <button onClick={() => handleSort(false)}>Sort -</button> <ul> {sortedList.map((item, index) => { return <li key={item}>{p}</li>; })} </ul> </div> ) |
Bu yazılan örnek daha kullanışlı sort + ‘ya art arta tıklamada gereksiz bir render olmaz. Sadece isIncrement verisi değiştikçe fonksyion yeniden sorting yapar.
Ne zaman kullanmalıyız? (useMemo ve useCallback)
Öncelikle uygulamanızı tasarlarken bu performans hooksları kullanılmamalı. Bu hookslar gerçekten ihtiyaç olan yerde kullanılmalısınız. Performans geliştirme aynı zamanda bazı sorumlulukları da peşinde getirir, bu geliştirmeler bedavaya yapılmaz. Diyelim her fonksiyonunuzu useCallback ile memoize ettik bu durumda bırakın performans geliştirmeyi daha da performansı düşürmüş oluruz. Component içinde inline fonksiyonların yeniden oluşması maliyetli bir işlem değildir, yeniden fonksiyonu oluşturduğunda garbage collector eskisini çöpe atar ama useCallback ile memoize edilirse çalışmaz ve memory birikir.
Peki hangi durumlarda kullanmalıyız?
- Referans karşılaştırmaları
- Maliyetli işlemler
1 2 3 4 5 6 |
var obj1 = {name:"Okan",surname : "Karadag"} var obj2 = {name:"Okan",surname : "Karadag"} console.log(obj1 === obj2); // false var t1 = 5; var t2 = 5; console.log(t1 === t2);//true |
Yukarıdaki örneğe baktığımızda obj1 ve obj2 eşit olmasına rağmen false döner. (derinlere girmiyorum şimdilik)
Bu noktada dependecy list bu açığı kapatıyor ve yapması gereken işi yapıyor. Veriler değiştiğinde eğer biz manuel olarak kontrol etseydik referans tipler için sonuç hep false çıkacağından her seferinde render olacaktır işte bu karşılaştırma işini react’a bırakıyoruz. Şimdi gereksiz render’ın doğru bir şekilde önüne nasıl geçilir bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
export const Options = ({ list, getItem }) => { useEffect(() => { const props = { list, getItem }; }, [list, getItem]); return <div>foobar</div>; }; export const Select = () => { const list = useMemo(() => {return ["Ronaldo", "Messi", "Mertens"]},[]); const getItem = useCallback((name) => { list.filter((item) => item.startsWith(name)); },[]); return <Options getItem={getItem} list={list} />; }; .... App: return ( <div className="App"> <Select /> <p>Counter: {counter}</p> <button onClick={() => handleChange()}>Increment</button> </div> ) |
Yukarıdaki örnekte eğer fonksiyon ve diziyi memoize etmeseydik counter her değiştiğinde ya da ana component render olduğunda yeni bir list ve getItem fonksiyonlarını oluşturacaktır. Yukarıdakine benzer durumlarda kullanmanızda bir sakınca yok. Ama kullanırken gerçekten ihtiyacınız olup olmadığını iyice analiz ediniz.
Gönderinin sonuna geldik başka bir makalede görüşmek üzere 🙂
Çok işime yaradı çok teşekkür ederim