Unarefresh ukurasa na kioo kinabaki tupu. Au labda unabonyeza kitufe na feni ya CPU inaanza kufanya kazi kwa kasi kubwa sana. Kisha console inatoa onyo linaloogopesha: Maximum update depth exceeded. React imepiga breki ghafla kwa sababu component yako imenaswa kwenye mzunguko usio na mwisho (infinite loop). Ni moja ya makosa ya kawaida zaidi katika programu za React, na kwa kawaida hutokana na kutoelewa vizuri wakati ambapo kodi yako inafanya kazi hasa.
Ili kuirekebisha, unahitaji kuelewa wakati kamili ambapo render inakuwa re-render, na kwa nini mabadiliko ya state lazima yakuepushe na njia ya render.
Jinsi React Inavyofanya Render ya Component Yako
React hujenga interface za watumiaji kwa kutumia components. Katika React ya kisasa, components hizo ni functions. Kila wakati React inapohitaji kuonyesha component yako kwenye kioo, inaita function hiyo tu. Ndani ya function hiyo, unaweza kutumia state ili kukumbuka mambo kati ya render. State inaiambia React ni data gani inayomilikiwa na component na, jambo la muhimu zaidi, ni lini kitu kimebadilika na UI inahitaji kusasishwa.
Wakati state inapobadilika, React inapanga render mpya. Function ya component inafanya kazi tena, inarudisha JSX mpya, na React inasasisha DOM ili ifanane. Katika matumizi ya kawaida, mzunguko huu hauna madhara. Unabonyeza kitufe, event handler inasasisha state, React inafanya re-render mara moja, na mtumiaji anaona maandishi au rangi mpya.
Kosa hutokea wakati render yenyewe inasababisha mabadiliko mengine ya state. Mabadiliko hayo mapya ya state yanasababisha render nyingine, ambayo inasababisha mabadiliko mengine ya state. React inavumilia hili kwa mizunguko kadhaa, kisha inatupa kosa la "maximum depth" ili kulinda kivinjari (browser) kisigande kabisa.
Mtazamo wa Haraka kuhusu State
Kabla ya kuchambua mzunguko huo, kumbuka jinsi hook ya useState inavyofanya kazi. Inakupa vitu viwili pekee: variable inayoshikilia thamani ya sasa, na function ya kubadilisha thamani hiyo.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
Hapa, message ni 'Welcome' kwenye render ya kwanza. Ukitoa wito wa setMessage('Goodbye') baadaye, React inatambua mabadiliko hayo, inaita MessageComponent tena, na UI sasa inaonyesha "Goodbye." Kila kitu kiko sawa kwa sababu hakuna kitu katika mwili wa component chenye kuitia setter hiyo kazi moja kwa moja. Mzunguko huanza wakati setter inafanya kazi wakati wa hatua ya render bila tukio la nje.
Kuita SetState Moja kwa Moja Ndani ya Mwili (Body)
Njia ya moja kwa moja zaidi ya kutengeneza mzunguko usio na mwisho ni kuita function ya state setter moja kwa moja ndani ya mwili wa component. Kwa sababu mwili wa component unatekelezwa kila wakati wa render, setter inafanya kazi kila wakati wa render. Mabadiliko hayo mapya ya state yanasababisha render nyingine. Mzunguko huo unazunguka milele.
Hivi ndivyo kosa hilo linavyoonekana:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
Kila wakati Counter inapofanya render, inaongeza count. React inafanya render tena ili kuonyesha nambari mpya, inaona setCount(count + 1) tena, na inaongeza mara nyingine tena. Suluhisho ni rahisi: usiwahi kuita state setter katika kiwango cha juu cha component yako wakati wa render. Updates za state zinapaswa kujibu matukio ya mtumiaji (user events) au side effects, siyo kitendo cha kuchora kioo. Hamisha update hiyo ndani ya event handler badala yake:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
Ubaguzi pekee wa kuita setters wakati wa render ni pale unapopiga hesabu ya state mpya kutoka kwa props, na hata hivyo unapaswa kutumia mfumo tofauti, kama vile kupata thamani hiyo moja kwa moja au kutumia useEffect kwa makusudi.
Kupitisha Wito wa Function Badala ya Reference
Sababu nyingine ya mara kwa mara ni kosa dogo la kimaandishi (typo) kwenye JSX. Unapoambatisha event handler, unahitaji kupitisha function yenyewe. Ukikosea na kuita function hiyo hapo hapo kwenye JSX, itafanya kazi mara moja wakati wa mzunguko wa render.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
Kwa kuandika handleToggle() ukiwa na mabano, hupe React function ya kuita baadaye mtumiaji anapobonyeza. Unaiita sasa hivi wakati React inajenga virtual DOM. Kwa kuwa handleToggle inasasisha state, component inafanya re-render. Wakati wa re-render hiyo, React inaona handleToggle() tena na kuiita tena. Mzunguko haumaliziki kamwe. Toleo sahihi huondoa mabano:
return <button onClick={handleToggle}>Toggle</button>;
Ikiwa unahitaji kupitisha argument, fungia wito huo ndani ya anonymous function:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
Tofauti hii inawachanganya hata watengenezaji wenye uzoefu wakati wa refactoring. Kuwa mwangalifu na mabano hayo.
Mtego wa Dependency wa useEffect
Effects ndipo mahali sahihi kwa ajili ya side effects kama vile kupata data, kusawazisha (syncing) na browser APIs, au kuendesha DOM kwa mkono. Lakini useEffect inafanya kazi baada ya React kukamilisha render kwenye kioo. Ikiwa effect yako inasasisha state, React itafanya re-render. Kawaida hiyo ni sawa. Inakuwa mzunguko wakati effect inafanya kazi baada ya kila render na kila wakati inasasisha state ile ile.
Fikiria mfano huu uliovunjika:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Kwa sababu hakuna array ya utegemezi (dependency array), effect hii inafanya kazi baada ya kila render. Inaweka user, jambo ambalo linasababisha render nyingine. Baada ya render hiyo, effect inafanya kazi tena na kuweka user tena. React inatambua mzunguko huo na kutoa kosa (error).
Suluhisho ni kumwambia React ni lini effect inapaswa kufanya kazi kwa kutoa array sahihi ya utegemezi. Ikiwa effect inapaswa kufanya kazi mara moja tu wakati wa kuunganishwa (mount), pitisha array tupu:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
Ikiwa effect inategemea prop au sehemu ya state, weka variable hiyo pekee kwenye array. Hata hivyo, kuwa mwangalifu. Kujumuisha variable inayobadilika kila render kutaunda mzunguko ule ule kupitia njia tofauti. Kwa mfano, ikiwa utajumuisha object literal kwenye utegemezi (dependencies) na object hiyo inauundwa upya kila render ya mzazi (parent render), effect itafanya kazi bila mwisho. Katika hali hizo, unaweza kuhitaji kuhamisha uundaji wa object nje ya component au kuifanya memoization.
Hatua za Vitendo za Kurekebisha Makosa (Debugging)
Unapokutana na kosa hili, stack trace inaweza kuonekana inachanganya kwa sababu React tayari imerudia mzunguko huo makumi ya mara. Anza kwa kusoma sehemu ya juu ya trace ili kupata component inayotajwa mara kwa mara. Kisha tafuta state setters katika sehemu hizi tatu:
- Kiini kikuu cha component, nje ya handler au hook yoyote.
- Sifa za matukio (event attributes) za JSX ambapo unaweza kuwa umeandika
handler()badala yahandler. - Hook za
useEffectambazo hazina array ya utegemezi au zinategemea marejeleo yasiyotabirika (unstable references).
Weka maoni (comment out) kwa muda kila state setter hadi kosa liache. Hilo litakuambia ni update gani hasa ndiyo chanzo cha tatizo. Ikiwa setter iko ndani ya effect, jiulize ikiwa unahitaji hata state hapo. Wakati mwingine watengenezaji huweka local state kutoka kwenye props ndani ya effect wakati wangeweza kutumia prop hiyo moja kwa moja kwenye JSX.
Somo Muhimu
Kosa la Maximum update depth siyo bug ya siri ya React. Ni kinga (safety net). Inamaanisha component yako inajaribu kujirender yenyewe badala ya kusubiri ishara ya nje. Vunja tabia ya kuchukulia renders kama matukio yanayopaswa kuzalisha state zaidi. Chukulia renders kama matokeo safi ya state, si kama visababishi. Weka state updates ndani ya event handlers, callbacks, au effects zenye utegemezi (dependencies) zilizochaguliwa kwa uangalifu, na hutawahi kuona kosa hili tena.
