നിങ്ങൾ പേജ് റിഫ്രഷ് ചെയ്യുന്നു, പക്ഷേ സ്ക്രീൻ ശൂന്യമായി തുടരുന്നു. അല്ലെങ്കിൽ ഒരു ബട്ടൺ ക്ലിക്ക് ചെയ്യുമ്പോൾ നിങ്ങളുടെ CPU ഫാൻ അതിന്റെ പരമാവധി വേഗതയിൽ കറങ്ങാൻ തുടങ്ങുന്നു. അപ്പോൾ കൺസോളിൽ ആ ഭയപ്പെടുത്തുന്ന മുന്നറിയിപ്പ് പ്രത്യക്ഷപ്പെടുന്നു: Maximum update depth exceeded. നിങ്ങളുടെ കോമ്പനന്റ് ഒരു ഇൻഫിനിറ്റ് ലൂപ്പിൽ (infinite loop) കുടുങ്ങിക്കിടക്കുന്നത് കൊണ്ട് React ബ്രേക്കുകൾ ഇട്ടു. ഇത് React ആപ്ലിക്കേഷനുകളിൽ സാധാരണയായി കണ്ടുവരുന്ന ഒരു പിശകാണ്, ഇത് സാധാരണയായി നിങ്ങളുടെ കോഡ് എപ്പോഴാണ് യഥാർത്ഥത്തിൽ പ്രവർത്തിക്കുന്നത് എന്നതിനെക്കുറിച്ചുള്ള ലളിതമായ തെറ്റിദ്ധാരണയിൽ നിന്നാണ് ഉണ്ടാകുന്നത്.
ഇത് പരിഹരിക്കുന്നതിന്, ഒരു റെൻഡർ (render) എപ്പോഴാണ് ഒരു റീ-റെൻഡർ (re-render) ആയി മാറുന്നതെന്നും, സ്റ്റേറ്റ് മാറ്റങ്ങൾ (state changes) എന്തുകൊണ്ട് റെൻഡർ പാത്തിൽ (render path) നിന്ന് ഒഴിവാക്കണമെന്നും നിങ്ങൾ കൃത്യമായി മനസ്സിലാക്കേണ്ടതുണ്ട്.
എങ്ങനെയാണ് React നിങ്ങളുടെ Component റെൻഡർ ചെയ്യുന്നത്
React യൂസർ ഇന്റർഫേസുകൾ നിർമ്മിക്കുന്നത് കോമ്പനന്റുകൾ (components) ഉപയോഗിച്ചാണ്. ആധുനിക React-ൽ, ആ കോമ്പനന്റുകൾ ഫങ്ക്ഷനുകളാണ് (functions). നിങ്ങളുടെ കോമ്പനന്റ് സ്ക്രീനിൽ കാണിക്കേണ്ടി വരുമ്പോഴെല്ലാം, React ആ ഫങ്ക്ഷൻ വിളിക്കുന്നു (call ചെയ്യുന്നു). ഫങ്ക്ഷനുള്ളിൽ, റെൻഡറുകൾക്കിടയിൽ കാര്യങ്ങൾ ഓർത്തു വെക്കാൻ നിങ്ങൾക്ക് state ഉപയോഗിക്കാം. ഏത് ഡാറ്റയാണ് കോമ്പനന്റിന്റേതെന്ന് React-നെ അറിയിക്കുന്നത് സ്റ്റേറ്റ് ആണ്, കൂടാതെ പ്രധാനമായും, എന്തെങ്കിലും മാറുമ്പോൾ UI അപ്ഡേറ്റ് ചെയ്യേണ്ടതുണ്ടോ എന്ന് ഇത് തീരുമാനിക്കുന്നു.
സ്റ്റേറ്റ് മാറുമ്പോൾ, React ഒരു പുതിയ റെൻഡർ ഷെഡ്യൂൾ ചെയ്യുന്നു. കോമ്പനന്റ് ഫങ്ക്ഷൻ വീണ്ടും പ്രവർത്തിക്കുന്നു, പുതിയ JSX നൽകുന്നു, തുടർന്ന് React അതിനനുസരിച്ച് DOM അപ്ഡേറ്റ് ചെയ്യുന്നു. സാധാരണ ഉപയോഗത്തിൽ, ഈ ചക്രം ദോഷകരമല്ല. നിങ്ങൾ ഒരു ബട്ടൺ ക്ലിക്ക് ചെയ്യുന്നു, ഒരു ഇവന്റ് ഹാൻഡ്ലർ (event handler) സ്റ്റേറ്റ് അപ്ഡേറ്റ് ചെയ്യുന്നു, React ഒരു തവണ റീ-റെൻഡർ ചെയ്യുന്നു, തുടർന്ന് ഉപയോക്താവിന് പുതിയ ടെക്സ്റ്റോ നിറമോ കാണാൻ സാധിക്കുന്നു.
ഒരു റെൻഡർ തന്നെ മറ്റൊരു സ്റ്റേറ്റ് അപ്ഡേറ്റിന് കാരണമാകുമ്പോഴാണ് ഈ പിശക് ഉണ്ടാകുന്നത്. ആ പുതിയ സ്റ്റേറ്റ് അപ്ഡേറ്റ് മറ്റൊരു റെൻഡറിന് കാരണമാകുന്നു, അത് വീണ്ടും മറ്റൊരു സ്റ്റേറ്റ് അപ്ഡേറ്റിന് കാരണമാകുന്നു. ബ്രൗസർ പൂർണ്ണമായും ഫ്രീസ് (freeze) ആകുന്നത് തടയാൻ React കുറച്ച് തവണ ഇത് അനുവദിക്കുകയും, അതിനുശേഷം 'maximum depth error' കാണിക്കുകയും ചെയ്യുന്നു.
സ്റ്റേറ്റിനെക്കുറിച്ച് ഒരു ലഘുവിവരണം
ലൂപ്പിനെക്കുറിച്ച് വിശദമായി പരിശോധിക്കുന്നതിന് മുമ്പ്, useState ഹുക്ക് (hook) എങ്ങനെയാണ് പ്രവർത്തിക്കുന്നതെന്ന് ഓർത്തെടുക്കുക. ഇത് നിങ്ങൾക്ക് കൃത്യമായി രണ്ട് കാര്യങ്ങളാണ് നൽകുന്നത്: നിലവിലെ മൂല്യം (value) സൂക്ഷിക്കുന്ന ഒരു വേരിയബിളും, ആ മൂല്യം മാറ്റുന്നതിനുള്ള ഒരു ഫങ്ക്ഷനും.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
ഇവിടെ, ആദ്യത്തെ റെൻഡറിൽ message എന്നത് 'Welcome' ആണ്. നിങ്ങൾ പിന്നീട് setMessage('Goodbye') എന്ന് വിളിച്ചാൽ, React ആ മാറ്റം ശ്രദ്ധിക്കുകയും, MessageComponent വീണ്ടും വിളിക്കുകയും ചെയ്യുന്നു, ഇപ്പോൾ UI "Goodbye" എന്ന് കാണിക്കുന്നു. കോമ്പനന്റ് ബോഡിയിൽ തന്നെ സെറ്റർ (setter) സ്വയമേവ വിളിക്കാത്തതിനാൽ എല്ലാം ശരിയായി പോകുന്നു. ഒരു എക്സ്റ്റേണൽ ഇവന്റ് ഇല്ലാതെ റെൻഡർ ഘട്ടത്തിൽ സെറ്റർ പ്രവർത്തിക്കുമ്പോഴാണ് ലൂപ്പ് ആരംഭിക്കുന്നത്.
കോമ്പനന്റ് ബോഡിയിൽ നേരിട്ട് SetState വിളിക്കുന്നത്
ഒരു ഇൻഫിനിറ്റ് ലൂപ്പ് ഉണ്ടാക്കാനുള്ള ഏറ്റവും നേരിട്ടുള്ള വഴി കോമ്പനന്റ് ബോഡിനുള്ളിൽ നേരിട്ട് ഒരു സ്റ്റേറ്റ് സെറ്റർ ഫങ്ക്ഷൻ വിളിക്കുക എന്നതാണ്. കോമ്പനന്റ് ബോഡി ഓരോ റെൻഡറിലും പ്രവർത്തിക്കുന്നതിനാൽ, സെറ്റർ ഓരോ റെൻഡറിലും പ്രവർത്തിക്കുന്നു. ആ പുതിയ സ്റ്റേറ്റ് അപ്ഡേറ്റ് മറ്റൊരു റെൻഡറിന് കാരണമാകുന്നു. ഈ ചക്രം എന്നെന്നേക്കുമായി തുടരുന്നു.
തെറ്റ് എങ്ങനെയായിരിക്കാം എന്ന് നോക്കാം:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
Counter റെൻഡർ ചെയ്യുമ്പോഴെല്ലാം, അത് count വർദ്ധിപ്പിക്കുന്നു. പുതിയ നമ്പർ കാണിക്കാൻ React വീണ്ടും റെൻഡർ ചെയ്യുന്നു, വീണ്ടും setCount(count + 1) കാണുന്നു, വീണ്ടും വർദ്ധിപ്പിക്കുന്നു. ഇതിനുള്ള പരിഹാരം ലളിതമാണ്: റെൻഡറിംഗ് സമയത്ത് നിങ്ങളുടെ കോമ്പനന്റിന്റെ ടോപ്പ് ലെവലിൽ (top level) ഒരിക്കലും ഒരു സ്റ്റേറ്റ് സെറ്റർ വിളിക്കരുത്. സ്റ്റേറ്റ് അപ്ഡേറ്റുകൾ ഉപയോക്താവിന്റെ ഇവന്റുകൾക്കോ സൈഡ് ഇഫക്റ്റുകൾക്കോ (side effects) പ്രതികരിക്കേണ്ടതാണ്, സ്ക്രീൻ വരയ്ക്കുന്ന പ്രക്രിയയോട് (painting the screen) അല്ല. പകരം ആ അപ്ഡേറ്റ് ഒരു ഇവന്റ് ഹാൻഡ്ലറിലേക്ക് മാറ്റുക:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
റെൻഡറിംഗിനിടെ സെറ്ററുകൾ വിളിക്കുന്നതിനുള്ള ഏക അപവാദം പ്രോപ്സിൽ (props) നിന്ന് പുതിയ സ്റ്റേറ്റ് കണക്കാക്കുമ്പോഴാണ്. എങ്കിലും അപ്പോഴും നിങ്ങൾ മൂല്യം നേരിട്ട് ഡെറിവ് ചെയ്യുകയോ (deriving the value) അല്ലെങ്കിൽ ബോധപൂർവ്വം useEffect ഉപയോഗിക്കുകയോ പോലുള്ള മറ്റൊരു രീതി ഉപയോഗിക്കണം.
ഒരു റഫറൻസിന് പകരം ഫങ്ക്ഷൻ കോൾ പാസ്സ് ചെയ്യുന്നത്
മറ്റൊരു സാധാരണ കാരണം JSX-ലെ ചെറിയൊരു ടൈപ്പോ (typo) ആണ്. നിങ്ങൾ ഒരു ഇവന്റ് ഹാൻഡ്ലർ ഘടിപ്പിക്കുമ്പോൾ, ഫങ്ക്ഷൻ തന്നെ പാസ്സ് ചെയ്യണം. നിങ്ങൾ അബദ്ധവശാൽ JSX-ൽ തന്നെ ആ ഫങ്ക്ഷൻ വിളിക്കുകയാണെങ്കിൽ (invoke), റെൻഡർ സൈക്കിൾ നടക്കുമ്പോൾ തന്നെ അത് ഉടൻ പ്രവർത്തിക്കും.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
ബ്രാക്കറ്റുകൾ ഉപയോഗിച്ച് handleToggle() എന്ന് എഴുതുന്നതിലൂടെ, ഉപയോക്താവ് ക്ലിക്ക് ചെയ്യുമ്പോൾ വിളിക്കാൻ ഒരു ഫങ്ക്ഷനല്ല നിങ്ങൾ React-ന് നൽകുന്നത്. പകരം React വിർച്വൽ DOM (virtual DOM) നിർമ്മിക്കുമ്പോൾ തന്നെ നിങ്ങൾ അത് വിളിക്കുകയാണ്. handleToggle സ്റ്റേറ്റ് അപ്ഡേറ്റ് ചെയ്യുന്നതിനാൽ കോമ്പനന്റ് വീണ്ടും റെൻഡർ ആകുന്നു. ആ റീ-റെൻഡറിംഗിനിടെ, React വീണ്ടും handleToggle() കാണുകയും അത് വീണ്ടും വിളിക്കുകയും ചെയ്യുന്നു. ഈ ലൂപ്പ് ഒരിക്കലും അവസാനിക്കില്ല. ശരിയായ രീതിയിൽ ബ്രാക്കറ്റുകൾ ഒഴിവാക്കണം:
return <button onClick={handleToggle}>Toggle</button>;
നിങ്ങൾക്ക് ആർഗ്യുമെന്റുകൾ (arguments) പാസ്സ് ചെയ്യണമെന്നുണ്ടെങ്കിൽ, ഒരു അനോനിമസ് ഫങ്ക്ഷനിൽ (anonymous function) ആ കോൾ ഉൾപ്പെടുത്തുക:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
റീഫാക്റ്ററിംഗ് (refactoring) സമയത്ത് പരിചയസമ്പന്നരായ ഡെവലപ്പർമാരെപ്പോലും ഈ വ്യത്യാസം കുഴപ്പിക്കാറുണ്ട്. ബ്രാക്കറ്റുകൾ ശ്രദ്ധിക്കുക.
useEffect ഡിപെൻഡൻസി ട്രാപ്പ് (Dependency Trap)
ഡാറ്റ ഫെച്ച് ചെയ്യുക (fetching data), ബ്രൗസർ API-കളുമായി സിങ്ക് ചെയ്യുക, അല്ലെങ്കിൽ മാനുവലായി DOM കൈകാര്യം ചെയ്യുക തുടങ്ങിയ സൈഡ് ഇഫക്റ്റുകൾക്ക് (side effects) അനുയോജ്യമായ ഇടമാണ് ഇഫക്റ്റുകൾ (effects). എന്നാൽ React റെൻഡർ സ്ക്രീനിൽ കമിറ്റ് (commit) ചെയ്തതിന് ശേഷമാണ് useEffect പ്രവർത്തിക്കുന്നത്. നിങ്ങളുടെ ഇഫക്റ്റ് സ്റ്റേറ്റ് അപ്ഡേറ്റ് ചെയ്യുന്നുണ്ടെങ്കിൽ, React വീണ്ടും റെൻഡർ ചെയ്യും. സാധാരണഗതിയിൽ അത് കുഴപ്പമില്ല. എന്നാൽ ഇഫക്റ്റ് ഓരോ റെൻഡറിന് ശേഷവും പ്രവർത്തിക്കുകയും എപ്പോഴും ഒരേ സ്റ്റേറ്റ് അപ്ഡേറ്റ് ചെയ്യുകയും ചെയ്യുമ്പോൾ അത് ഒരു ലൂപ്പായി മാറുന്നു.
ഈ തെറ്റായ പാറ്റേൺ ശ്രദ്ധിക്കുക:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
ഒരു dependency array ഇല്ലാത്തതിനാൽ, ഓരോ render-ന് ശേഷവും ഈ effect പ്രവർത്തിക്കുന്നു. ഇത് user സെറ്റ് ചെയ്യുന്നു, ഇത് വീണ്ടും ഒരു render-ന് കാരണമാകുന്നു. ആ render-ന് ശേഷം, effect വീണ്ടും പ്രവർത്തിക്കുകയും user വീണ്ടും സെറ്റ് ചെയ്യുകയും ചെയ്യുന്നു. ഈ ചക്രം തിരിച്ചറിഞ്ഞ് React എറർ കാണിക്കുന്നു.
ശരിയായ ഒരു dependency array നൽകിക്കൊണ്ട് effect എപ്പോഴാണ് പ്രവർത്തിക്കേണ്ടതെന്ന് React-നോട് പറയുന്നതാണ് ഇതിനുള്ള പരിഹാരം. effect മൗണ്ട് (mount) ചെയ്യുമ്പോൾ മാത്രം ഒരു തവണ പ്രവർത്തിക്കേണ്ടതെങ്കിൽ, ഒരു empty array നൽകുക:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
effect ഒരു prop-അതിനെ അല്ലെങ്കിൽ ഒരു state-നെ ആശ്രയിച്ചാണെങ്കിൽ, ആ variable മാത്രം array-യിൽ ഉൾപ്പെടുത്തുക. എന്നാൽ ശ്രദ്ധിക്കുക. ഓരോ render-നും മാറിക്കൊണ്ടിരിക്കുന്ന ഒരു variable ഉൾപ്പെടുത്തിയാൽ, അത് മറ്റൊരു വഴിയിലൂടെ അതേ ലൂപ്പ് തന്നെ വീണ്ടും സൃഷ്ടിക്കും. ഉദാഹരണത്തിന്, നിങ്ങൾ dependencies-ൽ ഒരു object literal ഉൾപ്പെടുത്തുകയും, ഓരോ parent render-നും ആ object വീണ്ടും സൃഷ്ടിക്കപ്പെടുകയും ചെയ്യുന്നുണ്ടെങ്കിൽ, effect അവസാനമില്ലാതെ പ്രവർത്തിച്ചുകൊണ്ടേയിരിക്കും. അത്തരം സാഹചര്യങ്ങളിൽ, object നിർമ്മാണം component-ന് പുറത്തേക്ക് മാറ്റുകയോ അല്ലെങ്കിൽ അത് memoize ചെയ്യുകയോ ചെയ്യേണ്ടി വന്നേക്കാം.
പ്രായോഗികമായ ഡീബഗ്ഗിംഗ് ഘട്ടങ്ങൾ (Practical Debugging Steps)
ഈ എറർ വരുമ്പോൾ, React ഇതിനകം തന്നെ ഈ ചക്രം ഡസൻ കണക്കിന് തവണ ആവർത്തിച്ചതുകൊണ്ട് stack trace കാണാൻ പ്രയാസമായി തോന്നാം. ഏത് component ആണ് ആവർത്തിച്ച് കാണപ്പെടുന്നത് എന്ന് കണ്ടെത്താൻ trace-ന്റെ മുകൾഭാഗം വായിച്ചു തുടങ്ങുക. തുടർന്ന് താഴെ പറയുന്ന മൂന്ന് സ്ഥലങ്ങളിൽ state setters ഉണ്ടോ എന്ന് പരിശോധിക്കുക:
- ഏതെങ്കിലും handler അല്ലെങ്കിൽ hook-ന് പുറത്തുള്ള component-ന്റെ പ്രധാന ഭാഗം (main body).
handler-ന് പകരംhandler()എന്ന് നിങ്ങൾ എഴുതിയിട്ടുണ്ടാകാൻ സാധ്യതയുള്ള JSX event attributes.- dependency array ഇല്ലാത്തതോ അല്ലെങ്കിൽ അസ്ഥിരമായ (unstable) references-നെ ആശ്രയിക്കുന്നതോ ആയ
useEffecthooks.
എറർ നിൽക്കുന്നത് വരെ ഓരോ state setter-ഉം താൽക്കാലികമായി comment out ചെയ്യുക. ഇത് ഏത് update ആണ് പ്രശ്നമുണ്ടാക്കുന്നത് എന്ന് കൃത്യമായി പറഞ്ഞുതരും. setter ഒരു effect-നുള്ളിലാണെങ്കിൽ, അവിടെ നിങ്ങൾക്ക് state ആവശ്യമുണ്ടോ എന്ന് സ്വയം ചോദിക്കുക. ചിലപ്പോൾ developers props-ൽ നിന്ന് local state ഒരു effect-നുള്ളിൽ സെറ്റ് ചെയ്യാറുണ്ട്, എന്നാൽ പകരം JSX-ൽ നേരിട്ട് ആ prop ഉപയോഗിക്കാവുന്നതാണ്.
യഥാർത്ഥ പാഠം (The Real Takeaway)
Maximum update depth error എന്നത് നിഗൂഢമായ ഒരു React ബഗ്ഗല്ല. അതൊരു സുരക്ഷാ കവചമാണ് (safety net). നിങ്ങളുടെ component ഒരു ബാഹ്യ സിഗ്നലിനായി (external signal) കാത്തുനിൽക്കുന്നതിന് പകരം സ്വയം വീണ്ടും render ചെയ്യാൻ ശ്രമിക്കുന്നു എന്നാണ് ഇതിനർത്ഥം. Render-കളെ കൂടുതൽ state ഉൽപ്പാദിപ്പിക്കേണ്ടുന്ന ഒരു event ആയി കാണുന്ന ശീലം ഒഴിവാക്കുക. Render-കളെ state-ന്റെ ഫലമായി (consequence) കാണുക, അല്ലാതെ അതിന്റെ കാരണമായി (cause) കാണരുത്. State updates എപ്പോഴും event handlers, callbacks, അല്ലെങ്കിൽ കൃത്യമായി തിരഞ്ഞെടുത്ത dependencies ഉള്ള effects എന്നിവയ്ക്കുള്ളിൽ മാത്രം സൂക്ഷിക്കുക, അപ്പോൾ നിങ്ങൾക്ക് ഈ എറർ ഇനി ഒരിക്കലും കാണേണ്ടി വരില്ല.
