Merhaba arkadaşlar, bugün başımı çok ağrıtan bir sorunu ve nasıl çözdüğümü anlatacağım. React'te performans optimizasyonu için React.memo'yu kullanıyoruz, değil mi? Amacı, props'lar değişmediği sürece component'ın gereksiz yere yeniden render edilmesini engellemek. Ama işte, bazen React.memo ile sarmaladığınız component, props'ları aynı olsa bile inatla yeniden render olmaya devam ediyor! Ben de bu durumu ilk gördüğümde "Hadi ya, memo çalışmıyor mu?" diye kafayı yemiştim. Meğerse sorun memo'da değil, props olarak geçirdiğimiz şeylerdeymiş.
Neden memo Bazen İşe Yaramıyor Gibi Görünür?
Bu sorunu yaşamamın sebebi, JavaScript'teki referans eşitliği kavramıydı. React.memo, props'ları sığ bir karşılaştırma (shallow comparison) ile kontrol ediyor. Yani, primitive değerler (string, number, boolean) için sorun yok. Ancak, fonksiyonlar (callback'ler), nesneler (objects) veya diziler (arrays) her render'da yeniden oluşturulduğu için, bellekteki adresleri (referansları) sürekli değişiyor. React.memo da bu yeni referansı görüp "Aa, props değişmiş!" diyerek component'ı maalesef yeniden render ediyor.
Örneğin, aşağıdaki gibi bir yapınız varsa, MemoizedChild her zaman yeniden render olacaktır:
Yukarıdaki örnekte, ParentComponent'taki butona her tıklandığında state (count) değişiyor ve component yeniden render oluyor. Bu render sırasında handleClick fonksiyonu ve childConfig nesnesi baştan yeni olarak oluşturuluyor. MemoizedChild'a giden props'lar aslında aynı içeriğe sahip olsa da, bellekteki adresleri farklı olduğu için React.memo farkı anlayamıyor ve child component'ı gereksiz yere render ediyor.
Çözüm: useCallback ve useMemo Kullanımı
İşte benim kullandığım en temiz çözüm: React'in bize sunduğu useCallback ve useMemo hook'ları. Bu hook'lar, sırasıyla fonksiyonların ve hesaplanmış değerlerin referanslarını, bağımlılık dizisi (dependency array) değişmediği sürece sabit tutmamızı sağlıyor.
Aynı örneği, bu hook'larla düzeltelim:
Şimdi, ParentComponent'taki butona tıkladığımızda, count state'i değişecek ve parent yeniden render olacak. Ancak, handleClick ve childConfig artık aynı referanslara sahip olacakları için, React.memo props'ların değişmediğini anlayacak ve MemoizedChild component'ını render etmeyecek! Konsola baktığınızda log'u sadece ilk seferde göreceksiniz.
Ne Zaman Kullanmalı, Ne Zaman Kullanmamalı?
Bu hook'lar sihirli değnek değil tabii. Her yere useCallback ve useMemo yazmak, kod karmaşıklığını arttırır ve hatta bazen performansı düşürebilir. Kullanım kararını şu şekilde verebilirsiniz:
useCallback: Alt componente geçirdiğiniz ve React.memo ile sarmaladığınız bir callback fonksiyonunuz varsa kullanın. Veya, bu fonksiyon useEffect gibi bir hook'un bağımlılık dizisindeyse, gereksiz tetiklenmeleri önlemek için kullanın.
useMemo: Hesaplaması "pahalı" (yoğun CPU kullanan) bir değeri (filtrelenmiş büyük bir liste gibi) veya React.memo'lu bir componente geçirdiğiniz nesne/diziyi memoize etmek için kullanın.
Unutmayın: Optimizasyon her zaman ölçülerek yapılmalı. React DevTools'un "Profiler" sekmesi, hangi component'ların neden render olduğunu görmek için harika bir araçtır.
Sonuç olarak, React.memo tek başına bazen yeterli olmuyor. Onun gücünü, useCallback ve useMemo ile birleştirdiğimizde gerçek anlamda etkili performans optimizasyonları yapabiliyoruz. Siz bu sorunla karşılaştınız mı? useMemo yerine nesneleri component dışında tanımlamak gibi farklı çözüm yollarınız var mı? Yorumlarda paylaşalım!
Bu sorunu yaşamamın sebebi, JavaScript'teki referans eşitliği kavramıydı. React.memo, props'ları sığ bir karşılaştırma (shallow comparison) ile kontrol ediyor. Yani, primitive değerler (string, number, boolean) için sorun yok. Ancak, fonksiyonlar (callback'ler), nesneler (objects) veya diziler (arrays) her render'da yeniden oluşturulduğu için, bellekteki adresleri (referansları) sürekli değişiyor. React.memo da bu yeni referansı görüp "Aa, props değişmiş!" diyerek component'ı maalesef yeniden render ediyor.
Örneğin, aşağıdaki gibi bir yapınız varsa, MemoizedChild her zaman yeniden render olacaktır:
JavaScript:
import React, { useState } from 'react';
const MemoizedChild = React.memo(({ onClick, config }) => {
console.log('Çocuk Component Render Edildi!');
return <button onClick={onClick}>Tıkla {config.theme}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// ❌ HER RENDER'DA YENİ BİR FONKSİYON!
const handleClick = () => {
console.log('Tıklandı');
};
// ❌ HER RENDER'DA YENİ BİR NESNE!
const childConfig = { theme: 'dark' };
return (
<div>
<button onClick={() => setCount(count + 1)}>Sayıyı Arttır: {count}</button>
<MemoizedChild onClick={handleClick} config={childConfig} />
</div>
);
}
Yukarıdaki örnekte, ParentComponent'taki butona her tıklandığında state (count) değişiyor ve component yeniden render oluyor. Bu render sırasında handleClick fonksiyonu ve childConfig nesnesi baştan yeni olarak oluşturuluyor. MemoizedChild'a giden props'lar aslında aynı içeriğe sahip olsa da, bellekteki adresleri farklı olduğu için React.memo farkı anlayamıyor ve child component'ı gereksiz yere render ediyor.
İşte benim kullandığım en temiz çözüm: React'in bize sunduğu useCallback ve useMemo hook'ları. Bu hook'lar, sırasıyla fonksiyonların ve hesaplanmış değerlerin referanslarını, bağımlılık dizisi (dependency array) değişmediği sürece sabit tutmamızı sağlıyor.
Aynı örneği, bu hook'larla düzeltelim:
JavaScript:
import React, { useState, useCallback, useMemo } from 'react';
const MemoizedChild = React.memo(({ onClick, config }) => {
console.log('Çocuk Component Render Edildi! - Artık Sadece Gerçekten Gerekince');
return <button onClick={onClick}>Tıkla {config.theme}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
// ✅ useCallback ile fonksiyon referansı korunuyor.
const handleClick = useCallback(() => {
console.log('Tıklandı');
}, []); // Boş bağımlılık dizisi = bu fonksiyon hiç değişmez.
// ✅ useMemo ile nesne referansı korunuyor.
const childConfig = useMemo(() => {
return { theme: 'dark' };
}, []); // Boş bağımlılık dizisi = bu nesne hiç değişmez.
return (
<div>
<button onClick={() => setCount(count + 1)}>Sayıyı Arttır: {count}</button>
<MemoizedChild onClick={handleClick} config={childConfig} />
</div>
);
}
Şimdi, ParentComponent'taki butona tıkladığımızda, count state'i değişecek ve parent yeniden render olacak. Ancak, handleClick ve childConfig artık aynı referanslara sahip olacakları için, React.memo props'ların değişmediğini anlayacak ve MemoizedChild component'ını render etmeyecek! Konsola baktığınızda log'u sadece ilk seferde göreceksiniz.
Bu hook'lar sihirli değnek değil tabii. Her yere useCallback ve useMemo yazmak, kod karmaşıklığını arttırır ve hatta bazen performansı düşürebilir. Kullanım kararını şu şekilde verebilirsiniz:
useCallback: Alt componente geçirdiğiniz ve React.memo ile sarmaladığınız bir callback fonksiyonunuz varsa kullanın. Veya, bu fonksiyon useEffect gibi bir hook'un bağımlılık dizisindeyse, gereksiz tetiklenmeleri önlemek için kullanın.
useMemo: Hesaplaması "pahalı" (yoğun CPU kullanan) bir değeri (filtrelenmiş büyük bir liste gibi) veya React.memo'lu bir componente geçirdiğiniz nesne/diziyi memoize etmek için kullanın.
Unutmayın: Optimizasyon her zaman ölçülerek yapılmalı. React DevTools'un "Profiler" sekmesi, hangi component'ların neden render olduğunu görmek için harika bir araçtır.
Sonuç olarak, React.memo tek başına bazen yeterli olmuyor. Onun gücünü, useCallback ve useMemo ile birleştirdiğimizde gerçek anlamda etkili performans optimizasyonları yapabiliyoruz. Siz bu sorunla karşılaştınız mı? useMemo yerine nesneleri component dışında tanımlamak gibi farklı çözüm yollarınız var mı? Yorumlarda paylaşalım!