React — Memoization Nedir ? React’ta Nasıl Kullanılır ? (React.memo, useMemo ve useCallback Kullanımı)
Bu yazının demo projesi GitHub’da bulunmaktadır — Demo projesinin bulunduğu GitHub Reposu → Repo
Merhaba arkadaşlar bugün sizlere React’ta optimizasyonu arttırmak için kullanılan React.memo, useMemo ve useCallback kullanımlarından bahsetmek istiyorum. Örneklere geçmeden önce yazıda da birçok kez belirteceğim Memoization teriminin ne anlama geldiğine bakalım.
Memoization Nedir?
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. — Wikipedia
Memoization en basit haliyle bir optimizasyon tekniği olarak geçmektedir. Bu optimizasyon tekniğinde bazen fonksiyonlar bazen değerler memoized edilir. (saklanır/kaydedilir).
Memoization ile siz bir fonksiyonu çağırdığınızda uygulamanız fonksiyondan dönen sonucu kaydeder. Böylelikle siz fonksiyonu çağırdığınızda içerideki işlemleri tekrardan çalıştırmaktansa, kaydetmiş olduğu sonucu direkt olarak döndürür. Daha net anlaşılması adına basit bir örnek verelim. Elimizde çarpma işlemi yapan bir fonksiyon olduğunu düşünelim:
production fonksiyonu bir parametre almaktadır. Bu örneğimizde parametreye 5 değerini verdiğimizde sonuç olarak bize 25 değerini döndürür. Eğer memoization tekniklerinden birini kullanarak bu fonksiyonu çağırırsak, 5 değerine karşılık gelen 25 değeri kaydedilir. Böylelikle bu fonksiyonu aynı parametre ile tekrar çağırmamız durumunda, parametre değerine karşılık gelen sonuç zaten elimizde olduğu için bize direkt olarak sonucu döndürür ve tekrardan çarpma işlemini gerçekleştirmez.
React’ta ise memoization tekniğini kullanarak, componentlerin veya fonksiyonların gereksiz yere tekrar tekrar render edilmesini veya oluşturulmasını engelleyebiliriz. Şimdi case’ler üzerinden React’ta memoization kullanımlarına bir bakalım.
Case 1 —Bir Componentin Gereksiz Yere Render Edilmesi (React.memo)
Uygulamamıza bir header, bir count state’i ve bir buton ekleyelim. Butona tıklandığında count değerinin artmasını sağlayalım. App.js componentimizi düzenleyelerek başlayalım.
/src
/components
Header.js
App.js
Buton ve count state’ini App.js içerisinde tanımlayalım. Aynı zamanda bir Header componenti oluşturalım ve Header componentini App.js içerisinde çağıralım. Bu Header componentimize bir görsel’in yolunu props olarak gönderelim. (Herhangi bir görselin yolunu App.js’te tanımlayabilirsiniz)
Header componentine göndermiş olduğumuz props’u, img tag’ının src özelliğine (attribute) ekleyelim.
Yaptığımız işlem aslında çok basit. Butona tıklandığı zaman count değerinin artırılmasını sağlarız.
Baktığınız zaman herhangi bir sorun yok gibi gözükmektedir. Header component’i içerisine console.log() ile bir log mesajı yazıp, tarayıcımızın developer tool’undan neler olup bittiğine bakalım.
Count state’ini ve increase metodunu App.js içerisinde tanımladığımız için, count değerini her değiştirdiğimizde App componenti tekrar render edilir. Header componentinde bir değişiklik yapmasak bile, App componentimiz tekrardan render edildiği için, Header componentin de tekrar render edildiğini görebiliriz. Bu gibi durumlar küçük uygulamalarda çok sorun gibi gözükmeselerde, uygulamanız büyüdükçe küçük gibi görünen bu sorunlar, uygulamanızın performansını ciddi şekilde etkileyebilir. Bu gibi durumlarda kurtarıcı olarak React.memo’yu kullanabiliriz.
React.memo
Eğer bir component React.memo ile çağrılırsa, React bu component’i render eder ve bu component’e gönderilen props değerlerini saklar/kaydeder (memoized). Bir sonraki render durumunda bu component’e gönderilen props değerleri, bir önceki render edildiğindeki props değerleri ile aynıysa component tekrar render edilmez. React.memo’yu kullanmanın birkaç farklı yolu bulunmaktadır:
Farklı bir yol:
Dilerseniz direkt olarak react içerisinden { memo } olarak çıkartarakta kullanabilirsiniz:
React.memo’yu ekledikten sonra uygulamamızı tekrar çalıştırıp tarayıcımızın konsol’una bakalım:
Count değerini değiştirdiğimiz zaman App component’i tekrar render edilmesine rağmen, içerisinde bulunan Header component’i tekrar render edilmedi. Bunun sebebi de yukarıda belirttiğim gibi, eğer component’e gönderilen props değerleri component render edildiği zaman bir önceki props değerleri ile aynıysa component’in render edilmesi pas geçilir. Böylelikle Header component’ine gönderilen props değerleri değişene kadar tekrar render edilmez.
Hatta gelin Header’a göndermiş olduğumuz görseli değiştiren bir buton ekleyelim ve test edelim :)
Gördüğünüz gibi imgPath değeri değişene kadar Header component’i tekrar render edilmemektedir 🤩
Senaryo 2 —Nesne Tipindeki Props’lardan Dolayı Componentin Gereksiz Yere Render Edilmesi (useMemo)
Şimdi bu durumun yeterli olmadığı başka bir konuya bakalım.
Elimizde bulunan kullanıcı listesinde arama yaparak kullanıcıları filtreleyebileceğimiz bir özellik ekleyelim.
2 yeni component oluşturalım. List ve ListItem olarak.
/src
/components
Header.js
List.js
App.js
App componentinde fetch API’yı kullanarak jsonplaceholder sayfasından bir kullanıcı listesi çekelim. Gelen kullanıcı listesini List componetine props ile yollayalım. Her bir kullanıcıyı da ListItem componentine yollayalım ve ekranda gösterelim. Elimizde bulunan kullanıcı listesinde arama yapabilmek için bir input ve bir buton ekleyelim. (Aşağıdaki kod bloğunun detaylarını, kodların altında bulabilirsiniz.)
- useEffect ile component sayfada/ekranda render edildikten sonra yapılması gerekenleri belirleyebiliriz. Bu örneğimizde kullanıcı listesini jsonplaceholder sayfasından çektik. userList state’ine çekmiş olduğumuz kullanıcı listesini atadık.
- Kullanıcının input alanına girmiş olduğu her bir değeri alabilmek için onChange eventini kullanırız. onChange’in karşına referans olarak vermiş olduğumuz handleText metodu ile girilen değeri text state’ine aktarırız.
- Kullanıcı butona bastığı anda search state’ine de kullanıcının girmiş olduğu input’u aktarırız.
- Kullanıcı butona tıkladığı anda search state’ine aktarılan değeri, elimizdeki kullanıcı listesinde bulunan her bir kullanıcının ismi ile karşılaştırılır. Eşleşen değerler bir listede döndürülür. Döndürülen liste filteredUsers’a aktarılır ve List componentine props olarak yollanır.
List componentine bakalım:
Gereksiz render durumlarını engellemek için React.memo’yu List ve ListItem componentlerimize ekledik. Uygulamamızı çalıştırıp buradaki soruna bakalım:
Search alanına girdiğimiz her bir karakterde handleText metodu tetiklenmektedir. Bu da App.js componentini tekrardan render edilmesine sebep olur. List componentimizi React.memo ile çağırmış olsak bile, List componentinin girilen her bir karakterde tekrardan render edildiğini görebiliriz.
List Componenti Neden Tekrar Render Edildi ? 😭
List componentine göndermiş olduğumuz userList props’unun tipi Object’tir. Yani nesnedir. Çünkü filteredUsers bir array’dir ve array’ler JavaScript’te Object tipindedir. Object tipinde gönderilen props’larda React.memo, nesnenin içerisinde bulunan değerleri değil memory’de tutulduğu adresleri karşılaştırır. List componenti bir önceki gönderilen userList ile yeni oluşturulan userList nesnesini Shallow comparison yaparak karşılaştırdığında, ikisinin memory’de tutulduğu adreslerin farklı olduğunu görür ve component’i tekrar render eder.
Shallow Comparison Nedir ?
Shallow compare does check for equality. When comparing scalar values (numbers, strings) it compares their values. When comparing objects, it does not compare their’s attributes — only their references are compared (e.g. “do they point to same object?). — Stackoverflow
Shallow comparison’da eğer karşılaştırılan tipler nesne (Object) ise içerisindeki değerleri değil referans değerleri karşılaştırılır. Eğer karşılaştırılan nesneler memory’de aynı adresi gösteriyorsa true göstermiyorsa false olarak değer döndürür.
Input alanına bir değer girdiğimiz zaman App.js tekrar render edildiği için filteredUsers tekrar oluşur. filteredUsers tekrardan oluştuğu için List componentine göndermiş olduğumuz userList her seferinde farklı bir adrese sahip olur. Bu yüzdende React.memo List componentine ilk seferde göndermiş olduğumuz userList array’inin tutulduğu adres ile render edildikten sonra gelen userList array’inin farklı adreste bulunduğunu gördüğü için List componentini tekrar render eder. Props olarak verdiğimiz array değişmediği halde List componentinin render edilmesini engelleyebilmek için useMemo kullanabiliriz.
useMemo
useMemo, bir fonksiyondan memoized edilen değeri döndürür. useMemo kullanımına bir bakalım:
Burada [a,b] dependency list olarak geçmektedir. İçerisinde 0 veya daha fazla değer bulundurabilir. Yukarıdaki örnekte memoizedValue fonksiyonu sadece [a,b] değerlerinin değişmesi durumunda tekrardan computeExpensiveValue fonksiyonunu tetikler. Bağlı olunan değerleri yani dependency list içerisine verilen değerleri useMemo takip eder. a ve b değerlerinin değişmemesi durumunda computeExpensiveValue fonksiyonundan dönen değer memozied edildiği için, useMemo fonksiyonu hiç çalıştırmadan direkt olarak kaydedilen değeri döndürür. useMemo ile takip edilen değer veya değerlerin değişmesi durumunda, fonksiyon tekrar çağrılır.
useMemo Ne Zaman Kullanılmalı?
useMemo ile fonksiyonlardan dönen değerler memoized edilir. Genellikle expensive dediğimiz durumlarda kullanılır. Örneğin fonksiyon çağrıldığı zaman çok fazla memory tüketen bir durumu düşünebilirsiniz. Çağırdığınız zaman fazla memory kullanımında bulunan bir fonksiyonunuz varsa, useMemo kullanabilirsiniz.
Bu örneğimizde search ve userList bu metodun takip edilen değerlerdir. filteredUsers sadece dependency olarak verilen değerlerin değişmesi durumunda tekrardan oluşturulur ve içerisindeki işlemler o zaman tetiklenir. search ve userList değişmediği sürece bu dependency list’e karşılık gelen bir değer kaydedilir ve dependency list içerisinde bulunan değerler değişmemişse direkt olarak memoized edilen sonuç döndürülür.
Gördüğünüz gibi biz search butonuna basmadığımız sürece search state’i güncellenmediği için filteredUsers tekrar tekrar oluşturulmamaktadır. Böylelikle List componentinin gereksiz yere render edilmesi engellenir.
Son olarak farklı bir problem ve çözümüne daha değinip yazıyı sonlandıralım :)
Senaryo 3 — Bir Fonksiyonun Props Olarak Gönderilmesi Durumunda Componentin Gereksiz Yere Render Edilmesi
Son olarak, input alanına girilen değerin resetlenmesini sağlayan ve tüm listenin tekrar gösterilmesini sağlayan bir buton ekleyelim.
App.js içerisinde ClearButton adında bir component’i çağıralım. Bu component içerisinde bulunan bir buton ile, App componentimizde tanımlamış olduğumuz search ve text state değerlerinin resetlenmesini sağlayalım.
/src
/components
ClearButton.js
Header.js
List.js
App.js
Öncelikle App.js’te bir fonksiyon tanımlayalım. Bu fonksiyonu ClearButton componentine içerisinde bulunan butona tıklandığında text ve search statelerine ilk değerlerini atasın. Bu fonksiyonu da ClearButton componentine yollayalım.
ClearButton component’i içerisine bir buton ekleyelim ve bu butonun onClick metoduna props ile göndermiş olduğumuz fonksiyonu verelim.
Butonumuzu da tanımladıktan sonra uygulamızı çalıştırıp yine her zaman yaptığımız gibi console.log ile tarayıcımızın konsolundan neler olup bittiğine bakalım.
Gördüğünüz gibi input alanına girdiğimiz her bir karakter sonucunda App.js render edildiği için ClearButton component’i de tekrar render edilmektedir. Bunun sebebi ClearButton componentimize props olarak göndermiş olduğumuz clearSearch fonksiyonu, App.js tekrar render edildiği için tekrar oluşmaktadır. JavaScript’te fonksiyonlarda Object tipindedir. useMemoda belirttiğim gibi, Object tipinde gönderilen props’larda React.memo referans karşılaştırması yaptığı için bir önceki render edildiğinde gönderilen memory adresi ile bir sonraki renderda gönderilen adres değerleri farklı olduğu için ClearButton componenti tekrar render edilir.
Bu soruna çözüm olarak useCallback’i kullanabiliriz.
useCallback
useCallback ile bir fonksiyonu memoized edebiliriz. Memoized edilen bu fonksiyon sadece bağımlı olduğu değerlerin (dependency list) değişmesi durumunda tekrardan oluşturulur. Nasıl kullanacağımıza bakalım:
Bu örneğimizde clearSearch fonksiyonumuzun tekrar oluşturulmasını gerektirecek bir dependency değer bulunmadığı için dependency list’i boş bıraktık. useCallback ile clearSearch fonksiyonunun memoized edilmesini sağlayarak, App.js’in render edilmesi durumunda ClearButton componentine gönderdiğimiz referans değerinin aynı kalmasını sağlayarak ClearButton’un gereksiz yere render edilmesini engellemiş oluruz.
Gördüğünüz gibi useCallback’i kullanmadan önce her input değeri girdiğimizde App.js render olduğu için ClearButton’a göndermiş olduğumuz clearSearch fonksiyonu da tekrardan oluşmaktaydı. Bu yüzden React.memo gönderilen props’un değiştiğini sanarak ClearButton’u tekrar render ediyordu. useCallback ile göndermiş olduğumuz fonksiyonu memoized ederek, clearSearch metodunun sadece dependency list içerisine yazılan değerlerin değişmesi durumunda tekrardan oluşturulmasını sağlamış olduk. Bu örneğimizde dependency list boş olduğu için bu fonksiyon yalnızca bir kere oluşturulur ve memoized edilir.
Özet Olarak
React.memo: Componentlerin gereksiz yere render edilmesini engelleyebilirsiniz.
useMemo: Bir fonksiyondan dönen değerleri memoized edebilirsiniz.
useCallback: Bir fonksiyonu memoized etmek için kullanabilirsiniz.
Demo kodlarının bulunduğu GitHub Reposu → Repo
Cidden çok uzun bir yazı oldu ben de bu kadar uzun süreceğini tahmin etmemiştim yazıya başlarken ama anlaşılması adına elimden geleni yapmaya çalıştım :)
Umarım faydalı bir yazı olmuştur. Eksik veya yanlış olduğunu düşündüğünüz kısımları bana iletirseniz çok sevinirim :)
İletişim kanalları: Twitter — LinkedIn — Mail
Bir sonraki yazıda görüşmek üzere!
Bu yazının referansları:
https://www.robinwieruch.de/react-usememo-hook