useCallback
useCallback
adalah sebuah React Hook yang memungkinkan Anda untuk meng-cache sebuah definisi fungsi diantara render ulang.
const cachedFn = useCallback(fn, dependencies)
Referensi
useCallback(fn, dependencies)
Panggil fungsi useCallback
di tingkat atas komponen Anda untuk meng-cache sebuah definisi fungsi diantara render ulang:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
Lihat contoh lainnya di bawah ini.
Parameter
-
fn
: Nilai fungsi yang ingin Anda simpan dalam cache. ini dapat menerima argumen apa saja dan mengembalikan nilai apa saja. React akan mengembalikan (tidak memanggil!) fungsi Anda kembali kepada Anda selama render awal. Pada render selanjutnya, React akan memberikan fungsi yang sama kepada Anda lagi jikadependencies
tidak berubah sejak render sebelumnya. Jika tidak, itu akan memberi Anda fungsi yang telah Anda lewati selama render saat ini, dan menyimpannya jika nanti dapat digunakan kembali. React tidak akan memanggil fungsi Anda. Fungsi tersebut dikembalikan kepada Anda agar Anda bisa memutuskan kapan dan apakah akan memanggilnya. -
dependencies
: Daftar dari semua nilai yang bersifat reaktif yang direferensikan di dalam kodefn
. Nilai-nilai reaktif termasuk props, state, dan semua variabel dan fungsi yang dideklarasikan langsung di dalam badan komponen. Jika linter Anda adalah dikonfigurasi untuk React, linter akan memverifikasi bahwa setiap nilai reaktif telah ditentukan dengan benar sebagai dependensi. Daftar dependensi harus memiliki jumlah item yang konstan dan ditulis dalam sebaris seperti[dep1, dep2, dep3]
. React akan membandingkan setiap dependensi dengan nilainya yang sebelumnya menggunakan algoritma perbandinganObject.is
.
Pengembalian
Pada render awal, useCallback
mengembalikan fungsi fn
yang telah Anda lewati.
Selama render berikutnya, useCallback akan mengembalikan fungsi fn
yang sudah tersimpan dari render terakhir (jika dependensi tidak berubah), atau mengembalikan fungsi fn
yang telah Anda lewati selama render ini.
Catatan Penting
useCallback
adalah sebuah Hook, jadi Anda hanya dapat memanggil useCallback di tingkat atas komponen Anda atau Hooks yang Anda buat sendiri. Anda tidak dapat memanggil useCallback di dalam looping atau kondisi. Jika Anda membutuhkannya, ekstrak komponen baru dan pindahkan state ke dalamnya.- React tidak akan membuang fungsi yang sudah di-cache kecuali ada alasan khusus untuk melakukannya. Sebagai contoh, pada saat pengembangan, React akan membuang cache ketika Anda mengedit file komponen Anda. Baik pada saat pengembangan maupun produksi, React akan membuang cache jika komponen Anda menunda (suspend) selama mounting awal. Di masa depan, React mungkin akan menambahkan fitur-fitur baru yang memanfaatkan pembuangan cache—Sebagai contoh, jika React menambahkan dukungan bawaan untuk virtualized lists di masa depan, maka masuk akal untuk membuang cache untuk item yang keluar dari virtualized table viewport tersebut. Hal ini seharusnya sesuai dengan ekspektasi Anda, Jika Anda mengandalkan
useCallback
sebagai optimasi kinerja. Jika tidak, sebuah state variable atau sebuah ref mungkin lebih sesuai.
Penggunaan
Melewati proses render ulang sebuah Komponen
ketika Anda mengoptimalkan kinerja rendering, Anda terkadang perlu meng-cache fungsi yang Anda berikan ke komponen turunan. Dan kemudian, Mari kita lihat terlebih dahulu sintaksisnya, lalu kita akan melihat dalam kasus mana ini berguna.
untuk meng-cache sebuah fungsi antara render ulang dari komponen Anda, bungkus definisi fungsi Anda ke dalam Hook useCallback
:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
Anda harus oper dua hal ke dalam useCallback
:
- Sebuah definisi fungsi yang ingin Anda cache diantara render ulang.
- Sebuah list dependensi termasuk setiap nilai yang ada di dalam komponen Anda yang digunakan dalam fungsi Anda.
Pada saat render awal, fungsi yang dikembalikan yang akan anda dapatkan dari useCallback
akan menjadi fungsi yang anda operkan.
Pada render berikutnya, React akan membandingkan dependensi dengan dependensi yang Anda operkan selama render sebelumnya. Jika tidak ada dependensi yang berubah (bandingkan dengan Object.is
), useCallback
akan mengembalikan fungsi yang sama seperti sebelumnya. Jika tidak, useCallback
akan mengembalikan fungsi yang telah Anda operkan di render ini.
Dengan kata lain, useCallback
meng-cache sebuah fungsi diantara render ulang sampai dependesi itu berubah.
Mari kita lihat contoh untuk melihat kapan ini berguna.
katakan jika Anda mengoper fungsi handleSubmit
di dalam ProductPage
ke komponen ShippingForm
:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
Anda akan menyadari bahwa mengganti nilai prop theme
membuat aplikasi terasa berhenti sejenak, tapi jika Anda hapus <ShippingForm />
dari JSX Anda, itu akan terasa lebih cepat. ini memberitahu Anda bahwa itu layak dicoba untuk optimasi komponen ShippingForm
.
Secara default, ketika sebuah komponen melakukan render ulang, React akan melakukan render ulang pada seluruh komponen turunannya secara rekursif inilah kenapa, ketika ProductPage
render ulang dengan theme
berbeda, komponen ShippingForm
juga render ulang. Ini bagus untuk komponen yang tidak memerlukan banyak perhitungan untuk render ulang. tapi jika anda telah memverifikasi bahwa render ulang itu lambat, Anda dapat memberitahu ShippingForm
untuk melewati render ulang ketika props itu sama seperti render sebelumnya dengan cara bungkus itu di dalam memo
:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Dengan perubahan ini, ShippingForm
akan melewati rendering ulang jika semua props itu sama seperti render sebelumnya. inilah kenapa meng-cache sebuah fungsi menjadi penting! misalkan Anda mendefinisikan handleSubmit
tanpa useCallback
:
function ProductPage({ productId, referrer, theme }) {
// setiap kali theme berubah, ini akan menjadi fungsi yang berbeda...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... jadi prop ShippingForm tidak akan pernah sama, dan komponen tersebut akan selalu melakukan re-render setiap kali terjadi perubahan tema */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Di Javascript, function () {}
atau () => {}
akan selalu membuat sebuah fungsi berbeda, serupa dengan bagaimana literal objek {}
selalu membuat objek baru. Normalnya, ini tidak akan menjadi sebuah masalah, tetapi ini berarti bahwa props ShippingForm
tidak akan pernah sama, dan optimasi memo
Anda tidak akan bekerja. disinilah useCallback
berguna:
function ProductPage({ productId, referrer, theme }) {
// Beritahu React untuk meng-cache fungsi Anda diantara render ulang...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...selama dependensi tidak berubah...
return (
<div className={theme}>
{/* ...ShippingForm akan menerima props yang sama dan dapat melewati render ulang */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Dengan membungkus handleSubmit
dalam useCallback
, Anda memastikan bahwa itu adalah fungsi yang sama antara setiap render ulang (sampai dependensi berubah). Anda tidak harus bungkus sebuah fungsi dalam useCallback
kecuali jika Anda melakukannya untuk beberapa alasan tertentu. Dalam contoh ini, alasannya adalah Anda oper itu ke komponen yang dibungkus dalam memo
, dan ini memungkinkannya untuk lewati render ulang. ada alasan lain mengapa Anda mungkin butuh useCallback
yang dijelaskan lebih lanjut di halaman ini.
Pendalaman
Anda sering melihat useMemo
berdampingan useCallback
. Mereka berdua berguna ketika Anda mencoba mengoptimalkan komponen turunan. mereka memungkinkan Anda memoize (atau, dengan kata lain, cache) sesuatu yang Anda telah oper ke bawah:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Memanggil fungsi Anda dan menyimpan ke dalam cache
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // meng-cache fungsi itu sendiri
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
Perbedaannya terletak pada apa yang Anda simpan dalam cache:
useMemo
meng-cache hasil dari panggilan fungsi Anda. Dalam contoh ini, itu meng-cache hasil pemanggilan fungsicomputeRequirements(product)
sehingga hasilnya tidak akan berubah kecualiproduct
berubah. ini memungkinkan Anda untuk oper objekrequirements
tanpa perlu render ulangShippingForm
. Jika diperlukan, React akan memanggil fungsi yang telah Anda oper selama rendering untuk menghitung hasilnya.useCallback
meng-cache fungsi itu sendiri. Berbeda denganuseMemo
, itu tidak akan memanggil fungsi yang Anda berikan. Sebaliknya, itu akan meng-cache fungsi yang telah Anda berikan sehinggahandleSubmit
sendiri tidak akan berubah kecualiproductId
ataureferrer
telah berubah. Ini memungkinan Anda untuk oper fungsihandleSubmit
ke bawah tanpa perlu render ulangShippingForm
. Kode Anda tidak akan dijalankan sampai pengguna mengirimkan formulir.
Jika Anda sudah terbiasa dengan useMemo
, Anda mungkin merasa terbantu dengan useCallback
seperti ini:
// implementasi sederhana (didalam React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Pendalaman
Jika aplikasi Anda seperti situs ini, dan sebagian besar interaksi bersifat kasar (seperti mengganti seluruh halaman atau sebagian), memoisasi biasanya tidak diperlukan. Di sisi lain, Jika aplikasi Anda seperti editor gambar, dan sebagian besar interaksi bersifat granular (seperti memindahkan bentuk), maka Anda mungkin menganggap memoisasi sangat membantu.
Caching fungsi dengan useCallback
hanya bermanfaat dalam beberapa kasus:
- Anda mengoper itu sebagai prop ke komponen yang dibungkus dalam
memo
. Anda ingin melewatkan rendering ulang jika nilai tidak berubah. Memoisasi memungkinkan Anda me-render ulang hanya jika dependensi berubah. - Fungsi yang Anda operkan nantinya digunakan sebagai dependensi dari suatu Hook. contoh, fungsi lain yang dibungkus dalam
useCallback
bergantung padanya, atau Anda bergantung pada fungsi ini dariuseEffect.
Jika tidak ada manfaat dari membungkus sebuah fungsi dengan useCallback
pada kasus lain. Tidak ada kerugian yang signifikan dari melakukan itu juga, sehingga beberapa tim memilih untuk tidak memikirkan kasus-kasus individu, dan memoize sebanyak mungkin. Kekurangannya adalah kode menjadi kurang mudah dibaca. Selain itu, tidak semua memoize efektif: sebuah nilai tunggal yang “selalu baru” sudah cukup untuk menghancurkan memoisasi untuk seluruh komponen.
Perhatikan bahwa useCallback
tidak mencegah membuat fungsi. Anda selalu membuat fungsi (dan itu bagus!), tapi React akan mengabaikannya dan memberi Anda kembali fungsi yang sudah di-cache jika tidak ada yang berubah.
Dalam praktiknya, Anda dapat menghindari penggunaan memoisasi yang berlebihan dengan mengikuti beberapa prinsip:
- Saat komponen membungkus komponen lain secara visual, biarkan accept JSX as children. Kemudian, jika komponen pembungkus memperbarui statenya sendiri, React tahu bahwa komponen-komponen turunan tidak perlu di-render ulang.
- Gunakanlah state lokal dan jangan lift state up lebih dari yang diperlukan. Jangan menyimpan state sementara seperti formulir atau apakah suatu item dihover di bagian atas pohon komponen atau dalam pustaka state global.
- jaga logika render Anda murni. jika sebuah komponen render ulang menyebabkan masalah or menghasilkan beberapa visual artifact yang mencolok, itu adalah sebuah bug di dalam komponen Anda! Perbaiki bug alih-alih menambahkan memoisasi.
- hindari efek yang tidak perlu yang mengubah state Kebanyakan masalah performa dalam aplikasi React disebabkan oleh rantai pembaruan yang berasal dari efek yang menyebabkan komponen Anda di-render berulang-ulang.
- Coba untuk menghapus dependensi yang tidak diperlukan dari Efek. Sebagai contoh, daripada memoisasi, seringkali lebih sederhana untuk memindahkan beberapa objek atau fungsi ke dalam Efek atau di luar komponen.
Jika suatu interaksi masih terasa lambat, gunakan profiler React Developer Tools untuk melihat komponen mana yang paling diuntungkan dari memoisasi, dan tambahkan memoisasi jika diperlukan. Prinsip-prinsip ini membuat komponen Anda lebih mudah untuk didebug dan dipahami, sehingga baik untuk diikuti dalam semua kasus. Secara jangka panjang, kami sedang meneliti melakukan memoisasi secara otomatis untuk menyelesaikan masalah ini sekali dan untuk selamanya.
Contoh 1 dari 2: Melewati pengulangan rendering dengan useCallback
dan memo
Dalam contoh ini, komponen ShippingForm
diperlambat dengan sengaja agar Anda bisa melihat apa yang terjadi ketika komponen React yang Anda render benar-benar lambat. Cobalah untuk menambahkan nilai counter dan mengubah tema.
Meningkatkan counter terasa lambat karena memaksa ShippingForm
yang melambat untuk render ulang. Ini diharapkan karena counter berubah, sehingga Anda perlu memperbarui pilihan pengguna di layar.
Selanjutnya, coba ubah tema. Berkat useCallback
bersama dengan memo
, meskipun ada penundaan buatan, itu tetap cepat! ShippingForm
menghindari render ulang karena fungsi handleSubmit
tidak berubah. fungsi handleSubmit
tidak berubah karena kedua productId
dan referrer
(dependensi useCallback
Anda) tidak berubah sejak render terakhir.
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) { // Bayangkan ini mengirim permintaan... console.log('POST /' + url); console.log(data); }
Mengupdate state dari callback yang telah dimemoisasi
Terkadang, Anda mungkin perlu memperbarui state berdasarkan state sebelumnya dari sebuah callback yang dimemoisasi.
Fungsi handleAddTodo
menyebutkan todos
sebagai dependensi karena itu menghitung todos selanjutnya dari todos tersebut:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Anda biasanya ingin fungsi yang telah dimemoisasi memiliki sedikit dependensi. Ketika Anda membaca beberapa state hanya untuk menghitung state berikutnya, Anda dapat menghapus dependensi tersebut dengan cara memberikan fungsi updater sebagai gantinya:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ No need for the todos dependency
// ...
Disini, bukannya menjadikan todos
sebagai dependensi dan membacanya di dalamnya, Anda memberikan instruksi tentang bagaimana untuk memperbarui state (todos => [...todos, newTodo]
) ke React. Baca lebih lanjut tentang updater functions.
Mencegah Efek untuk terlalu sering dipicu
Kadang-kadang, Anda mungkin ingin memanggil sebuah fungsi dari dalam sebuah Effect:
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();
// ...
Hal ini menimbulkan masalah. Setiap nilai reaktif harus dideklarasikan sebagai dependensi dari Effect Anda. sebagai dependensi, itu akan membuat Effect Anda terus-menerus terhubung ulang ke ruang obrolan:
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Masalah: dependensi ini akan berubah setiap render
// ...
Untuk memecahkan masalah ini, Anda dapat membungkus fungsi yang perlu Anda panggil dari sebuah Effect dengan useCallback
:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Hanya berubah ketika roomId berubah
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ hanya berubah ketika createOptions berubah
// ...
Ini memastikan bahwa fungsi createOptions
sama antara pengulangan render jika roomId
sama. Namun, lebih baik lagi untuk menghilangkan kebutuhan akan dependensi fungsi. Pindahkan fungsi Anda ke dalam Effect:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ Tidak perlu menggunakan useCallback atau fungsi dependensi!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ hanya berubah ketika roomId berubah
// ...
Sekarang kode Anda lebih sederhana dan tidak perlu useCallback
. Pelajari lebih lanjut tentang menghapus dependensi Effect.
Mengoptimalkan sebuah Custom Hook
Jika Anda menulis custom Hook, disarankan untuk membungkus setiap fungsi yang dikembalikannya dengan useCallback
:
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,
};
}
Ini memastikan bahwa pengguna dari Hook yang Anda buat dapat mengoptimalkan kode mereka sendiri jika diperlukan.
Troubleshooting
Setiap kali komponen saya di-render, useCallback
mengembalikan fungsi yang berbeda
Pastikan Anda telah menyebutkan senarai dependensi sebagai argumen kedua!
Jika Anda lupa untuk menyebutkan senarai dependensi, useCallback akan mengembalikan fungsi baru setiap kali:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Mengembalikan fungsi baru setiap kali: tidak ada senarai dependensi
// ...
Ini adalah versi yang sudah diperbaiki dengan memberikan senarai dependensi sebagai argumen kedua:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Tidak mengembalikan fungsi baru tanpa alasan yang diperlukan
// ...
Jika ini tidak membantu, maka masalahnya adalah setidaknya satu dari dependensi Anda berbeda dari render sebelumnya. Anda dapat melakukan debugging masalah ini dengan secara manual mencetak dependensi ke konsol:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Anda dapat mengklik kanan pada senarai dari render yang berbeda di konsol, kemudian pilih “Simpan sebagai variabel global” untuk keduanya. Dalam asumsi bahwa yang pertama disimpan sebagai temp1
dan yang kedua disimpan sebagai temp2
, Anda kemudian dapat menggunakan konsol browser untuk memeriksa apakah setiap dependensi di kedua senarai tersebut sama:
Object.is(temp1[0], temp2[0]); // Apakah dependensi pertama sama antara senarai-senarai tersebut?
Object.is(temp1[1], temp2[1]); // Apakah dependensi kedua sama antara senarai-senarai tersebut?
Object.is(temp1[2], temp2[2]); // ... dan seterusnya untuk setiap dependensi ...
Ketika Anda menemukan dependensi mana yang memecah memoisasi, entah cari cara untuk menghapusnya, atau mememoisasi juga.
Saya perlu memanggil useCallback
untuk setiap item di dalam perulangan, tapi itu tidak diizinkan
Misalkan komponen Chart
dibungkus dengan memo
. Anda ingin melewati pengulangan setiap Chart
dalam daftar ketika komponen ReportList
di render ulang. Namun, Anda tidak dapat memanggil useCallback
dalam sebuah perulangan:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 Anda tidak dapat memanggil useCallback di dalam perulangan seperti ini:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Sebaliknya, buat komponen tersendiri untuk setiap item, dan tempatkan useCallback
di sana:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Panggil useCallback di level atas:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Anda juga bisa menghapus useCallback
pada contoh terakhir dan sebaliknya mengganti Report
itu sendiri dengan memo
. Jika prop item
tidak berubah, Report
akan melewati proses render ulang sehingga Chart
akan melewati proses render ulang juga:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});