useCallback
useCallback
, render’lar arasında bir fonksiyon tanımını önbelleğe almanızı sağlayan React Hook’udur.
const cachedFn = useCallback(fn, dependencies)
Referans
useCallback(fn, dependencies)
Render’lar arasında bir fonksiyon tanımını önbelleğe almak (memoize) için bileşeninizin en üst kapsamında useCallback
’i çağırın:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
Daha fazla örnek için aşağıya bakın.
Parametreler
-
fn
: Önbelleğe almak istediğiniz fonksiyondur. Herhangi bir argüman alıp herhangi bir değer döndürebilir. React, fonksiyonunuzu ilk render sırasında size geri döndürür (çağırmaz!). Sonraki render’larda,dependencies
dizinin öğeleri son render’dan bu yana değişmediyse aynı fonksiyonu tekrar verir. Değiştiyse mevcut render sırasında ilettiğiniz işlevi size verir ve tekrar kullanılabilmek için saklar. React, fonksiyonunuzu çağırmaz. Fonksiyon size geri döndürülür ve böylece onu ne zaman çağırıp çağırmayacağınıza karar verebilirsiniz. -
dependencies
:fn
kodu içerisinde başvurulan reaktif değerlerin listesidir. Reaktif değerler; prop, state ve doğrudan bileşeninizin gövdesinde bildirilen değişkenleri ve fonksiyonları içerir. Linter’ınız React için yapılandırılmışsa, kullanılan reaktif değerin bağımlılık olarak doğru bir şekilde belirtildiğini denetler. Bağımlılık listesi sabit sayıda öğe içermeli ve[dep1, dep2, dep3]
şeklinde doğrudan yazılmalıdır. ReactObject.is
karşılaştırma algoritmasını kullanarak her bir bağımlılığı önceki değeriyle karşılaştırır.
Dönüş değeri
İlk render’da, kendisine ilettiğiniz fn
fonksiyonunu döndürür.
Sonraki render’larda, ya son render’dan önce kaydedilmiş (bağımlılıkları değişmediyse) fn
fonksiyonunu ya da o anki render’da ilettiğiniz fn
fonksiyonunu döndürür.
Dikkat edilmesi gerekenler
useCallback
bir Hook olduğundan, yalnızca bileşeninizin en üst kapsamında ya da kendi Hook’larınızda çağırabilirsiniz. Döngülerin ve koşulların içinde çağıramazsınız. Eğer çağırmak zorunda kalırsanız yeni bir bileşene çıkarın ve state’i ona taşıyın.- React, özel bir nedeni olmadıkça önbelleğe alınan fonksiyonu temizlemez. Örneğin, geliştirme aşamasında bileşeninizin dosyasını düzenlediğinizde React önbelleği temizler. Hem geliştirme hem de production aşamasında, ilk render sırasında bileşeniniz askıya alınırsa React önbelleği temizler. Gelecekte, önbelleğin temizlenmesinden yararlanan daha fazla özellik ekleyebilir—örneğin, sanallaştırılmış listeler için yerleşik destek eklenirse, sanallaştırılmış tablonun görünüm alanından dışarı kaydırılan öğeler için önbelleği temizlemek mantıklı olacaktır.
useCallback
Hook’una performans optimizasyonu olarak güveniyorsanız bu beklentilerinizi karşılamalıdır. Aksi durumlar için state değişkeni veya ref daha uygun olabilir.
Kullanım
Bileşenlerin yeniden render işlemini atlama
Render performansını optimize ederken, alt bileşenlere ilettiğiniz fonksiyonları bazen önbelleğe almanız gerekebilir. Önce bunun nasıl yapılacağına ilişkin sözdizime bakalım, ardından hangi durumlarda faydalı olacağını görelim.
Bileşeninizdeki fonksiyonları render işlemleri arasında önbelleğe almak için tanımını useCallback
Hook’una sarın:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
useCallback
’e iki şey geçmeniz gerekir:
- Render’lar arasında önbelleğe almak istediğiniz fonksiyon.
- Bileşen içerisinde tanımlı olan ve fonksiyonunuzun içerisinde kullandığınız tüm değerleri içeren bağımlılık listesi.
İlk render’da, useCallback
’in döndüğü fonksiyon ilettiğiniz fonksiyonun kendisidir.
Sonraki render’larda, bağımlılıklar önceki render’daki değerleriyle karşılaştırılır. Bağımlılıkların hiçbirisi değişmemişse (Object.is
ile karşılaştırılarak karar verilir), useCallback
aynı fonksiyonu döner. Ancak değişmişse, useCallback
o anki render’da ilettiğiniz fonksiyonu döner.
Diğer bir deyişle, useCallback
bir fonksiyonu bağımlılıkları değişene kadar önbellekte tutar ve her istendiğinde yeni fonksiyon oluşturmak yerine aynı fonksiyonu döner.
Bunun ne zaman faydalı olabileceğini görmek için bir örnek üzerinden ilerleyelim.
ProductPage
’den ShippingForm
bileşenine handleSubmit
fonksiyonunu ilettiğinizi düşünün:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
theme
prop’unu değiştirmenin uygulamayı anlık dondurduğunu fark etmişsinizdir fakat JSX’inizden <ShippingForm />
bileşenini kaldırırsanız hızlandığını hissedebilirsiniz. Bu size ShippingForm
bileşenini optimize etmenin denemeye değer olduğunu gösterir.
React varsayılan davranış olarak bir bileşen yeniden render edildiğinde tüm alt bileşenlerini özyinelemeli olarak yeniden render eder. Bu nedenle ProductPage
farklı bir theme
ile yeniden render edildiğinde ShippingForm
bileşeni de render edilir. Fakat render işleminin yavaş olduğuna kanaat getirdiyseniz, ShippingForm
’i memo
ile sarmalayarak prop’ları değişmediği takdirde render’ı atlamasını söyleyebilirsiniz:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Bu değişiklikle birlikte ShippingForm
, prop’ları son render ile aynıysa render’ı atlar. Burası fonksiyon önbelleklemenin önemli hale geldiği zamandır! handleSubmit
fonksiyonunu useCallback
olmadan tanımladığınızı varsayalım:
function ProductPage({ productId, referrer, theme }) {
// `theme` her değiştiğinde bu farklı bir fonksiyon olur...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... böylece ShippingForm'un prop'ları her zaman farklı olur ve her seferinde yeniden render eder */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
JavaScript’te fonksiyon tanımları (function () {}
ya da () => {}
) her zaman farklı fonksiyon oluşturur. Bu durum {}
nesne değişmezinin her zaman yeni bir nesne oluşturmasına benzerdir. Normalde, bu bir sorun teşkil etmez ancak ShippingForm
prop’ları asla aynı olmayacağı için memo
optimizasyonu asla çalışmaz. useCallback
bu noktada işe yarar hale gelir:
function ProductPage({ productId, referrer, theme }) {
// React'a fonksiyonu render'lar arasında önbelleğe almasını söyler...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...bunu bağımlılıklar değişmediği sürece yapar...
return (
<div className={theme}>
{/* ...ShippingForm aynı prop'u alır ve render'ı atlar */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
handleSubmit
’i useCallback
’e sararak, yeniden render’lar arasında aynı fonksiyon kalmasını sağlarsınız (bağımlılıklar değişene kadar). Spesifik bir sebebiniz olmadığı sürece bir fonksiyonu useCallback
sarmanız gerekmez. Bu örnekte kullanma sebebiniz, memo
ile sarılmış bir bileşene fonksiyon iletmenizdir ve yeniden render edilmesini engeller. useCallback
’e ihtiyaç duymanızın bu sayfada ayrıntılı olarak açıklanan başka nedenleri de vardır.
Derinlemesine İnceleme
useMemo
’yu useCallback
ile birlikte sıkça görürsünüz. Her ikisi de alt bileşeni optimize etmeye çalıştığınızda kullanışlıdır. Alt bileşene ilettiğiniz şeyi önbelleğe almanıza olanak sağlarlar:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Fonksiyonunuzu çağırır ve sonucunu önbelleğe alır
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Fonksiyonun kendisini önbelleğe alır
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
Aralarındaki fark önbelleğe aldıkları şeyle alakalıdır:
useMemo
fonksiyonunuzun sonucunu önbelleğe alır. Bur örnekte,computeRequirements(product)
çağrısının sonucunuproduct
değişene kadar önbelleğe alır. BöyleceShippingForm
’ı gereksiz yere render etmedenrequirements
nesnesini aşağıya iletir. Gerektiğinde React sonucu hesaplamak için render sırasında geçtiğiniz fonksiyonu çağırır.useCallback
fonksiyonun kendisini önbelleğe alır.useMemo
’nun aksine, sağladığınızn fonksiyonu çağırmaz. Bunun yerine, verdiğiniz fonksiyonu önbelleğe alır ve böyleceproductId
vereferrer
değişene kadarhandleSubmit
’ın kendisi değişmez. Bu,ShippingForm
’u gereksiz yere render etmedenhandleSubmit
fonksiyonunu aşağıya geçmenize olanak tanır. Kullanıcı formu gönderene kadar kodunuz çalışmaz.
useMemo
’ya zaten aşinaysanız useCallback
’i şu şekilde düşünmek yardımcı olabilir:
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
useMemo
ve useCallback
arasındaki fark hakkında daha fazla bilgi edinin.
Derinlemesine İnceleme
Uygulamanız bu site gibiyse ve basit etkileşimler barındırıyorsa (sayfayı veya bir bölümünü tamamen değiştirmek gibi), önbelleğe almak genelde gereksizdir. Ancak uygulamanız daha çok çizim editörüne benziyorsa ve etkileşimlerin çoğu ayrıntılıysa (şekilleri taşımak gibi), son derece faydalı olacaktır.
Bir fonksiyonu useCallback
ile önbelleğe almak yalnızca birkaç durum için faydalıdır:
memo
’ya sarılmış bir bileşene prop olarak geçersiniz. Değer değişmediyse render’ı atlamak istersiniz. Önbelleğe alma işlemi, yalnızca bağımlılıkları değiştiyse yeniden render tetikler.- Geçtiğiniz fonksiyon daha sonra bazı Hook’ların bağımlılığı olarak kullanırsınız. Örneğin,
useCallback
’e sarılmış başka bir fonksiyonun bağımlılığıdır ya dauseEffect
için bu fonksiyona bağımlısınızdır.
Diğer durumlarda fonksiyonları useCallback
’e sarmanın hiçbir faydası olmaz. Bunu yapmanın da önemli bir zararı yoktur ve bu nedenle bazı ekipler durumları teker teker düşünmektense mümkün olduğunca önbelleğe almayı tercih ederler. Dezavantajı ise kodu daha az okunabilir hale getirmesidir. Aynı zamanda, önbellekleme her şeyde etkili değildir: her zaman yeni olan tek bir değer tüm bileşen için önbelleklemeyi bozmaya yeterlidir.
useCallback
’in fonksiyon oluşturmayı engellemediğini unutmayın. Her zaman yeni fonksiyon oluşturursunuz (ve bu iyidir), ancak React bunu yok sayarak hiçbir şey değişmediği takdirde önbelleğe alınan fonksiyonu geri verir.
Pratikte, birkaç ilkeyi takip ederek önbelleğe alma işlemlerinin çoğunu gereksiz hale getirebilirsiniz:
- Bir bileşen diğerlerini görsel olarak sardığında, JSX’i alt bileşen (children) olarak kabul etmesine izin verin. Böylece sarmalayıcı bileşen kendi state’ini güncellerse, React alt bileşenleri yeniden render etmesine gerek olmadığını bilir.
- Yerel state’i tercih edin ve state’i gereğinden fazla üst bileşene taşımayın. Form gibi geçici state’leri veya bileşenin tıklanma durumunu ağacınızın en üstünde yada global state yönetim kütüphanesinde saklamayın.
- Render mantığınızı saf tutun. Bileşeni yeniden render etmek bir soruna yol açıyorsa veya göze çarpan bir görsel farklılıklar oluşturuyorsa, bileşeninizde bir bug vardır! Önbelleğe almak yerine bug’ı çözün.
- State’i gereksiz güncelleyen Efektlerden kaçının. React uygulamalarındaki çoğu performans sorunu, Efektlerden kaynaklanan ve bileşenlerinizin tekrar tekrar render edilmesine neden olan güncelleme zincirlerinden meydana gelir.
- Efektlerinizden gereksiz bağımlılıkları kaldırın. Örneğin, önbelleğe almak yerine ilgili nesneyi veya fonksiyonu Efektin içine ya da bileşenin dışına taşımak genellikle daha basittir.
Buna rağmen gecikmeli gelen spesifik bir etkileşim varsa, hangi bileşenlerin önbelleğe almadan en çok yararlandığını görmek için React Developer Tools profiler aracını kullanın ve gerekli şeyleri önbelleğe alın. Bu ilkeler, bileşenlerinizin anlaşılır olmasını sağlar ve hataların ayıklanmasını kolaylaştırır. Durum farketmeksizin takip etmek faydalıdır. Uzun vadede bunu kökten çözmek için önbelleğe almayı otomatik hale getirmeyi araştırıyoruz.
Örnek 1 / 2: useCallback
ve memo
ile yeniden render işlemini atlama
Bu örnekte, ShippingForm
bileşeni yapay olarak yavaşlatılmıştır, böylece bileşen render’ının gerçekten yavaş olduğunda ne olacağını görebilirsiniz. Sayacı artırmayı ve temayı değiştirmeyi deneyin.
Sayacı arttırmak, yavaşlatılmış ShippingForm
’u yeniden render’a zorladığı için yavaş hissettirir. Bu beklenen durumdur çünkü sayaç değişti ve bu nedenle kullanıcının yeni seçimini ekrana yansıtmanız gerekir.
Ardından temayı değiştirmeyi deneyin. memo
ve useCallback
birlikteliği sayesinde yapay yavaşlatmadan etkilenmez! ShippingForm
render’ı atlar çünkü handleSubmit
fonksiyonu değişmemiştir. handleSubmit
fonksiyonunun değişmeme sebebi, hem productId
hem de referrer
(useCallback
bağımlılığınız) değişkenlerinin son render’dan beri değişmemesidir.
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Bunun istek attığını hayal edin... console.log('POST /' + url); console.log(data); }
Önbelleğe alınmış callback’den state güncelleme
Bazen önbelleğe alınmış callback’den önceki değerine bağlı olarak state’i güncellemeniz gerekebilir.
handleAddTodo
fonksiyonu todos
state’ini bağımlılık olarak belirtir çünkü sonraki todos
’u ondan hesaplar:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Genellikle önbelleğe alınmış fonksiyonların mümkün mertebe az bağımlılığa sahip olmasını istersiniz. State bağımlılığını yalnızca bir sonraki state değerini hesaplamak için kullanıyorsanız, güncelleyici fonksiyon (updater function) ile değiştirerek bu bağımlılığı kaldırabilirsiniz:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ todos bağımlılığına ihtiyaç yok
// ...
Burada, todos
’u bağımlılık haline getirmek ve fonksiyonun içinde okumak yerine, state’i nasıl güncelleyeceğinizle ilgili talimatı (todos => [...todos, newTodo]
) React’e iletirsiniz. Güncelleyici fonksiyonlar hakkında daha fazla bilgi edinin.
Efektlerin çok sık tetiklenmesini önleme
Bazen Efekt içinde fonksiyon çağırmak isteyebilirsiniz:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
// ...
Bu bir sorun yaratır. Her reaktif değer, Efektinizin bağımlılığı olarak bildirilmelidir. Ancak, createOptions
’ı bağımlılık olarak bildirirseniz, Efektinizin sohbet odasına sürekli olarak yeniden bağlanmasına neden olur:
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Sorun: Bu bağımlılık her render'da değişir
// ...
Bu sorunu çözmek için, Efekt içinde çağırmanız gereken işlevi useCallback
’e sarabilirsiniz:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Yalnızca roomId değiştiğinde değişir
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Yalnızca createOptions değiştiğinde değişir
// ...
roomId
’nin aynı olması durumunda createOptions
fonksiyonunun render’lar arasında aynı kalmasını sağlar. Bununla birlikte, fonksiyon bağımlılık ihtiyacını ortadan kaldırmak daha da iyidir. Fonksiyonunuzu Efektin içine taşıyın:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ useCallback ve fonksiyon bağımlılıklarına gerek yok!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Yalnızca roomId değiştiğinde değişir
// ...
Artık kodunuz daha basittir ve useCallback
’e ihtiyaç duymaz. Efekt bağımlılıklarını kaldırmakla ilgili daha fazla bilgi edinin.
Özel Hook’u Optimize Etme
Özel Hook yazıyorsanız, döndürdüğü tüm fonksiyonları useCallback
içine sarmanız önerilir:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
Bu kullanım, Hook’unuzu kullanan kişilerin gerektiğinde kendi kodlarını optimize edebilmelerini sağlar.
Sorun giderme
Bileşenim her render olduğunda, useCallback
farklı bir fonksiyon döndürüyor
Bağımlılık dizisini ikinci argüman olarak belirttiğinizden emin olun!
Bağımlılık dizisini unutursanız useCallback
her seferinde yeni bir fonksiyon döndürür:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Her seferinde yeni fonksiyon döndürür: bağımlılık dizisi yok
// ...
Bağımlılık dizisinin ikinci argüman olarak iletildiği düzeltilmiş hali şu şekildedir:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Gereksiz yere yeni bir fonksiyon döndürmez
// ...
Bu işe yaramazsa, sorun bağımlılıklarınızdan en az birinin önceki render işleminden farklı olmasıdır. Bağımlılıklarınızı manuel olarak konsola yazdırırsanız sorunun sebebini tespit edebilirsiniz:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Daha sonra konsolda farklı render’larda yazdırılan dizilere sağ tıklayıp her ikisi için de “Store as a global variable“‘ı seçebilirsiniz. temp1
ve temp2
olarak kaydedildiklerini varsayarsak, her iki dizide bulunan bağımlılıkların aynı olup olmadığını kontrol etmek için tarayıcı konsolunu kullanabilirsiniz:
Object.is(temp1[0], temp2[0]); // İlk bağımlılık diziler arasında aynı mı?
Object.is(temp1[1], temp2[1]); // İkinci bağımlılık diziler arasında aynı mı?
Object.is(temp1[2], temp2[2]); // ... her bağımlılık için devam eder ...
Önbellek mekanizmasını kıran bağımlılığı bulduğunuzda, ya bir yolunu bulup kaldırın ya da önbelleğe alın.
Bir döngüdeki her liste öğesi için useCallback
’i çağırmam gerekiyor ama yapmama izin vermiyor
Chart
bileşeninin memo
içine sarıldığını varsayalım. ReportList
bileşeni yeniden render edildiğinde listedeki her Chart
’ın yeniden render işlemi atlamak istiyorsunuz. Ancak, döngü içerisinde useCallback
çağıramazsınız:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 `useCallback`i bu şekilde döngüde çağıramazsınız:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Bunun yerine, her öğeyi bileşene çıkarın ve useCallback
’i bu bileşene yerleştirin:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ useCallback'i en üst kapsamda çağırın:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Alternatif olarak, son kod parçasındaki useCallback
’i kaldırabilir ve yerine Report
bileşeninin kendisini memo
’ya sarabilirsiniz. item
prop’u değişmezse, Report
yeniden render’ı atlar. Bu nedenle Chart
’da yeniden render edilmez:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});