Mafunzo mengi ya utendaji wa React huishia na ushauri mbaya uleule: funga kila kitu ndani ya useMemo na useCallback kisha uone kama umemaliza kazi. Ikiwa umefuata ushauri huo, pengine umeifanya programu yako kuwa nzito zaidi. Hook hizi si za bure. Kila moja hutenga kumbukumbu (memory), hulinganisha utegemezi (dependencies), na kuhifadhi thamani zilizowekwa kwenye cache. Zikitumiwa bila kusudi, zinakuwa mzigo badala ya uboreshaji.
Hebu tuichambue hii hadi kwenye kile kinachojali kweli.
Kile Kila Hook Kinachofanya Kweli
useMemo hukumbuka thamani (value). Unampa function inayofanya kazi nzito, na inarudisha matokeo. Wakati wa render inayofuata, ikiwa utegemezi (dependencies) wako haujabadilika, React huruka hesabu hiyo na kurudisha matokeo ya zamani.
useCallback hukumbuka function. Haikufanyi kazi ya function hiyo kwa ajili yako. Inarudisha tu instance ile ile ya function kati ya render mbalimbali mradi tu utegemezi wake ubaki vilevile.
Hiyo ndiyo tofauti nzima. Moja huhifadhi thamani iliyopatikana (computed value). Moja huhifadhi rejea (reference). Kuchanganya hivi husababisha kodi inayonekana kama imeboreshwa lakini inafanya kazi sawa na kodi ambayo haijafungwa, huku ikitumia kumbukumbu zaidi.
Kwa Nini Utambulisho wa Function Unavuruga Mti Wako
Wakati component inapofanya re-render, React hutekeleza mwili mzima wa function tena. Kila variable huundwa upya. Kila inline function hupata anwani mpya kabisa kwenye kumbukumbu.
Katika JavaScript, function mbili zenye mantiki (logic) ile ile hazilingani. () => {} === () => {} hutoa matokeo ya false. Sheria hiyo hiyo inatumika kwa objects na arrays. Ikiwa component yako mama (parent) inafafanua handleSubmit na kuipitisha kwa child, child hiyo inapokea prop mpya kwenye kila render. Hata kama child imefungwa ndani ya React.memo, haiwezi kujua kuwa function mpya inafanya kitu kile kile kama ile ya zamani. Rejea (reference) imebadilika, hivyo child inafanya re-render.
Hili ndilo tatizo la msingi ambalo useCallback ilijengwa kutatua. Sio kuhusu kasi. Ni kuhusu uthabiti (stability).
Wakati useMemo Inapostahili Kutumika
Unahitaji useMemo unapofanya kazi ambayo ni nzito kulingana na ukweli na unaweza kuona ucheleweshaji (lag) unaopimika.
Fikiria kuhusu kuchuja (filtering) seti kubwa ya data. Ikiwa una jedwali lenye maelfu ya mistari na sehemu ya kutafutia (search input), unaweza kuandika kitu kama hiki ndani ya component yako:
const visibleRows = rows.filter(r => r.name.includes(query));
Bila useMemo, loop hiyo hufanya kazi kwenye kila render. Ikiwa mtumiaji anabonyeza kitufe kinachofungua au kufunga sidebar, parent inafanya re-render, na chujio (filter) lako hufanya kazi tena hata kama rows na query hazijawahi kubadilika. Kwenye seti kubwa ya data, kigugumizi hicho kinaonekana.
useMemo inarekebisha hili kwa kufunga matokeo:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
Sasa React inarudia tu chujio hilo wakati utegemezi (dependencies) zinapobadilika kweli.
Mantiki hiyo hiyo inatumika kwa hesabu tata za hisabati, kubadilisha majibu ya API kuwa mifumo inayofaa kwa chati, au kupata state ambayo vinginevyo ingepaswa kuhesabiwa upya kila wakati.
Kuna matumizi mengine ya pili, ambayo si ya wazi sana. Ikiwa unatengeneza object au array ndani ya component na kuiweka kwenye array ya utegemezi ya useEffect, unaweza kusababisha effect hiyo kufanya kazi kwa bahati mbaya kwenye kila render. Objects na arrays za inline hupata utambulisho mpya kila wakati, hivyo effect huona utegemezi iliyobadilika na kuwaka tena. Kuifanya object hiyo kuwa memoized kwa kutumia useMemo huifanya rejea (reference) iwe thabiti na kuruhusu effect yako kufanya kazi tu wakati data ya msingi inapobadilika kweli.
Wakati useCallback Inapokuwa ya Lazima
useCallback ina umuhimu mkubwa zaidi unapopitisha handlers kwenye child components ambazo zimeboreshwa kwa React.memo.
Wazia component mama inayoshikilia counter. Pia inatoa (renders) orodha ya child ambayo ni nzito:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
Kila wakati count inapobadilika, Parent inafanya re-render. handleItemClick mpya huundwa. Kwa sababu ExpensiveList inapokea rejea mpya ya prop, inafanya re-render pia. Ikiwa ExpensiveList imefungwa ndani ya React.memo, uboreshaji (memoization) huo unapotea kabisa kwa sababu prop ya function imebadilika.
useCallback huhifadhi rejea:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
Sasa ExpensiveList inafanya re-render tu wakati inapohitaji kweli.
Hali nyingine muhimu inahusisha useEffect. Ikiwa effect inajiunga (subscribes) na function iliyofafanuliwa ndani ya component yako, na function hiyo inabadilisha utambulisho kila render, effect hiyo itajiondoa na kujiunga tena mara kwa mara. Kuifanya function hiyo kuwa memoized huifanya effect iwe thabiti.
Mtego wa Dependency Array na Stale Closures
Hook zote mbili zinategemea dependency arrays, na hapa ndipo hitilafu (bugs) nyingi hujificha.
Ukimwacha variable kwenye dependency array, function au value yako iliyofanyiwa memoization itatumia toleo la zamani la variable hiyo. Hii ni stale closure. UI inaweza kuonyesha data mpya, lakini callback yako bado inatazama state ya renders tatu zilizopita. Suluhisho ni rahisi lakini ni rahisi kulisahau wakati wa code reviews: jumuisha kila thamani inayotumika ndani ya hook ambayo inaweza kubadilika.
Endesha rule ya ESLint ya react-hooks/exhaustive-deps. Itakukuta makosa ya wazi ya kusahau vitu. Lakini usichukulie kama roboti. Elewa kwa nini kila dependency ni muhimu.
Kodi Iliyofichika ya Over-Optimization
Wanaoanza mara nyingi huweka kila function na kila value ndani ya hooks hizi kwa sababu inaonekana salama. Tabia hiyo huleta matokeo hasi.
React lazima ihifadhi cached values kwenye memory. Kila render, lazima ipitie dependency array yako na kulinganisha kila item kwa kutumia Object.is. Ulinganishi huo ni rahisi, lakini si bure. Ukifunika event handler rahisi kama onClick={() => setOpen(true)} ndani ya useCallback, unalipia gharama za memory na CPU ili kuepuka kutengeneza function ambayo ingekuwa rahisi sana kuipatia nafasi (allocate).
Hooks pia huongeza kelele. Code iliyofunikwa na useMemo na useCallback ni ngumu zaidi kusoma na ngumu zaidi kudumisha. Kila dependency array ni stale closure inayoweza kukupata wakati wowote.
Kanuni halisi ya msingi ni isiyo na mvuto lakini yenye ufanisi: andika plain code kwanza. Optimize tu pale unapokuwa na uthibitisho wa tatizo. Tumia React DevTools Profiler kutambua ni components zipi zinatumia rasilimali nyingi (expensive) na ni renders zipi zinapoteza muda. Ikiwa render inachukua chini ya milisekunde chache, hakuna mtumiaji atakayeona, na memoization yako haitatatua kitu chochote.
Hitimisho
useMemo ni kwa ajili ya expensive values. useCallback ni kwa ajili ya stable function references. Hakuna hook inayofanya component yako i-render kwa haraka yenyewe; zinazuia kazi zisizo za lazima zinazofuata. Anza bila hizo, pima kwa kutumia zana halisi, na uziongeze pale tu profiler inapoonyesha bottleneck. Clean code inayojirudia (rerenders) mara chache mara nyingi itakuwa bora kuliko code iliyozidishwa ufundi (over-engineered) inayofanya memoization kwa kila kitu.
