useMemo ve useCallback
İlk paylaşımım olan bu yazımda React’ın memoization hooklarından useMemo ve useCallback’i ele almak istedim. Birçok kişide kafa karışıklığına neden olan, nerede ve nasıl kullanılması gerektiğinden emin olunamayan bu hookları örnekleri ile birlikte açıklayacağım.
Eğer orta veya büyük ölçekli React projelerinde görev alıyorsanız bu hooklar ile büyük ihtimalle karşılaşmışsınızdır. Hatta tam olarak işlevlerini bilmeden projede kullanılıyor diye kullanmış da olabilirsiniz fakat bu kullanımların çoğunun gereksiz ve memory leak’lere neden olduğunu söylemem gerekir. Lafı daha fazla uzatmadan useMemo ile useCallback’ten ve asıl kullanım amaçlarından bahsedelim.
Memory Leak: Kodların kullanmış olduğu hafıza bloğu ile işinin sona ermesine rağmen o hafıza bloğunu bırakmamasına denir.
useMemo
Örnekte de gözüktüğü şekilde useMemo, bir “create” fonksiyonu ile dependency array (bağımlı değişkenler dizisi) alır ve bir memoized value (hafızaya alınmış değer) dönüşü yapar. Örnekteki isimlendirmelerle devam edecek olursak memoizedValue değişkeni, dependency array içerisindeki a veya b değerleri değişmediği sürece aynı değeri hafızada tutacaktır. Eğer a veya b değerlerinden en az birisi değişirse “create” fonksiyonu çalışıp yeni bir değer dönüşü yapacaktır. Hesaplanması zaman alabilecek ve performans sorunlarına neden olacak değerleri hesaplamak için kullanılır (computationally expensive calculations).
useCallback
useCallback ise argüman olarak inline callback ile dependency array alır ve dönüş değeri olarak da memoized callback (hafızaya alınmış callback fonksiyonu) dönüşü yapar. useMemo’da da olduğu gibi memoizedCallback değişkeninin tutuğu callback sadece dependency array içerisindeki a veya b değişkenleri değiştiği zaman değişir. Ayrıca useCallback, gereksiz render etme işlemlerini önlemek için reference equality yöntemine göre optimize edilmiş alt componentlere callback fonksiyonu geçerken işimize yarar.
useMemo ve useCallback’in Kullanım Senaryoları
- Referential Equality (Referans Eşitliği)
- Computationally Expensive Calculations (Pahalı hesaplamalar)
useMemo ve useCallback gibi memoization hooklarını kullanmak maliyet doğurabilir. Bu yüzden gerçekten ihtiyaç duyulan noktalarda sorumluluk alınarak kullanılması gerekmektedir.
Referential Equality
JavaScript dilinde abstract equality (==) ve strict equality (===) olarak iki çeşit eşitlik operatörü vardır. React ise genellikle Object.is(object1, object2) eşitlik kontrolünü kullanır fakat çok detaya girmeden bu yapı ile strict equality’nin oldukça benzer olduğunu söyleyebiliriz.
React’ta referential equalty’nin önemli olduğu 2 durum vardır. Bunlardan biri dependency array diğeri ise React.memo’dur.
Dependency Array
React’ta ki bazı hooklar, yapısında dependency array bulundurur. Bunlardan en bilindikleri ise useEffect, useMemo ve useCallback şeklinde sıralanabilir. useEffect üzerinden örnekler ile devam edecek olursak (örnekler anlaşılır olması için basit tutulmuştur):
Yukarıdaki örnekte firstProp ile secondProp değiştiğinde useEffect’in tetiklenmesi amaçlanıp functionA’nın tekrar çalıştırılması beklenmektedir. Fakat JavaScript’in çalışma mantığı nedeniyle propObject her render sonrasında yenilenecektir. Bu nedenle de React, useEffect’in dependency array’inde bulunan değişkenin değiştiğini düşünüp firstProp ve secondProp değişmemiş olsa da useEffect’i çalıştıracaktır. Bu yüzden istenmeyen durumlar oluşabilmektedir ve iki farklı şekilde bu durumu düzeltmemiz mümkündür.
Bunlardan ilki ve mümkünse uygulanmasını tavsiye edeceğim çözüm yolu şu şekildedir.
Fakat bazı durumlarda geçtiğimiz proplar primitive (string, number vb.) olmayabilir. Yani function, object veya array gibi tiplere sahip olduklarında useMemo veya useCallback kullanarak yukarıda bahsettiğimiz sorunu düzeltebiliriz.
Bu kullanım şekli de aslında useMemo ve useCallback hooklarının ortaya çıkış nedenini göstermektedir.
React.memo
Yine bir örnek üzerinden ilerleyecek olursak:
Örnekte de gözüktüğü üzere Counter komponentimiz, Button adlarında iki adet komponent barındırmaktadır. Button komponentlerinden birine her tıkladığımızda Counter komponenti içerisindeki statelerden biri değişeceği için her iki Button komponenti de render edilecektir. Tıklamış olduğumuz yani değerinin değişmesini istediğimiz Button komponentinin içerisinde güncel count değerine ulaşacağımız için bu komponentin render olması bize bir sorun teşkil etmezken diğer Button komponenti gereksiz bir şekilde tekrar render edilecektir. Bu sorunu düzeltmek için Button komponentini React.memo ile sarmalamamız gerekmektedir.
React.memo(component, areEqual) aslında bir high order component’dir (üst katman bileşeni) ve gözüktüğü şekilde 2 adet argüman alır. İlk argüman sarmalamak istediğimiz komponent olurken ikinci argümanı ise kullanılması zorunlu olmayan özel karşılaştırma (areEqual) fonksiyonudur. Ayrıca React.memo normalde yüzeysel olarak (shallowly compare) komponent içerisindeki propları karşılaştırıp render işlemine karar verirken areEqual fonksiyonu sayesinde karşılaştırma işlemini özelleştirebiliriz. Detaylı bilgi için…
Yukarıdaki şekilde sarmaladıktan sonra Button komponenti React.memo sayesinde sadece propları değiştiği zaman tekrar render edilecektir. Tam da burada küçük bir noktayı daha düzeltmemiz gerekiyor çünkü Button komponentinin proplarından onClick’e verilen değişkenler non-primitive (function, object vb.) tipte olduğu için state değişiminden sonra yeniden oluşturulacaklar ve gereksiz render etme işlemini önleyememiş olacağız. Bu durumu düzeltmek için de devreye useCallback’i giriyor (duruma göre useMemo da kullanılabilir).
Yaptığımız bu değişiklik ile gereksiz render etme işlemini önlemiş oluyor ve React.memo’yu sağlıklı bir şekilde kullanmış oluyoruz.
React.memo optimizasyonu bir maliyet doğurabileceğinden dolayı kullanımına karar vermeden önce emin olmalıyız ve doğru bir şekilde kullanmalıyız. Ayrıca React’ın hızlı çalışma yapısı sayesinde ciddi sorunlar çıkarmayan gereksiz render işlemleri göz ardı edilebilir.
Computationally Expensive Calculations
Maliyet doğurabilecek hesaplamaları useMemo içerisinde yapmak bize performans sağlayacaktır. useMemo’nun asıl ortaya çıkış amacı da bu tarz maliyetli işlemleri gereksiz tekrarlardan sakınmak içindir. Yine bir örnek verecek olursak (tam olarak kullanışlı bir senaryo olmasa da anlaşılır olmasını amaçladığım bir örnek):
Yukarıdaki kod satırlarında gözüktüğü şekilde olası render işlemlerinde calculatePrimes tekrar tekrar çalışacaktır ve asal sayıların hesaplanmasının maliyet oluşturacağını varsayarsak projemize ciddi bir yük olacaktır.
Bu şekilde useMemo ile sarmaladığımızda sadece dependency array içerisindeki değerlerin değişmesi durumda hesaplanacaktır. Yani gereksiz render işlemlerinde hesaplama yapmayıp hafızadaki hesaplanmış değeri döndürecektir.
Sonuç
useMemo ve useCallback hooklarının kullanımı performans sağlayabilirken maliyetinin de olabileceğini tekrar vurgulamak istiyorum. Özellikle aynı proje üzerinde çalıştığınız arkadaşlarınız için karışıklık doğurabilir ya da dependency array’de yapacağınız yanlışlık ile memoize edilmiş değerlerin garbage collector tarafından temizlenmesini engelleyerek performans sorunlarına neden olabilirsiniz. Eğer ölçümlerinizi ve kullanımınızı doğru yaparsanız başta da söylediğim gibi performans faydaları sağlayabilirsiniz.
Umarım faydalı bir yazı olmuştur :)