आप पेज रिफ्रेश करते हैं और स्क्रीन खाली ही रहती है। या शायद किसी बटन पर क्लिक करने से आपके CPU का पंखा तेज़ चलने लगता है। फिर console में वह डरावनी चेतावनी आती है: Maximum update depth exceeded. React ने ब्रेक लगा दिए हैं क्योंकि आपका component एक infinite loop में फंस गया है। यह React applications में सबसे आम errors में से एक है, और यह आमतौर पर इस बात की गलतफहमी से पैदा होता है कि आपका code वास्तव में कब चलता है।
इसे ठीक करने के लिए, आपको उस सटीक क्षण को समझने की आवश्यकता है जब एक render, re-render बन जाता है, और क्यों state changes को render path से बाहर रहना चाहिए।
React आपके Component को कैसे Render करता है
React components से user interfaces बनाता है। आधुनिक React में, वे components functions होते हैं। हर बार जब React को स्क्रीन पर आपका component दिखाना होता है, तो वह बस उस function को call करता है। Function के अंदर, आप renders के बीच चीज़ों को याद रखने के लिए state का उपयोग कर सकते हैं। State React को बताती है कि कौन सा data component का है और, सबसे महत्वपूर्ण बात यह है कि, कब कुछ बदल गया है और UI को update करने की आवश्यकता है।
जब state बदलती है, तो React एक नया render schedule करता है। Component function फिर से चलता है, नया JSX return करता है, और React उसे match करने के लिए DOM को update करता है। सामान्य उपयोग में, यह cycle हानिरहित है। आप एक बटन पर क्लिक करते हैं, एक event handler state को update करता है, React एक बार re-render करता है, और user को नया text या color दिखाई देता है।
यह error तब आता है जब एक render अपने आप में एक और state update को trigger कर देता है। वह नया state update एक और render को trigger करता है, जो एक और state update को trigger करता है। React कुछ दर्जन cycles तक इसे सहन करता है, और फिर browser को पूरी तरह से freeze होने से बचाने के लिए maximum depth error throw कर देता है।
State पर एक नज़र
Loop का विश्लेषण करने से पहले, याद करें कि useState hook कैसे काम करता है। यह आपको ठीक दो चीज़ें देता है: एक variable जो current value को hold करता है, और उस value को बदलने के लिए एक function।
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
यहाँ, पहले render पर message की value 'Welcome' है। यदि आप बाद में setMessage('Goodbye') call करते हैं, तो React बदलाव को नोट करता है, MessageComponent को फिर से call करता है, और UI अब "Goodbye" दिखाता है। सब कुछ ठीक है क्योंकि component body में कुछ भी setter को अपने आप call नहीं कर रहा है। Loop तब शुरू होता है जब setter बिना किसी external event के render phase के दौरान fire हो जाता है।
Body में सीधे SetState को Call करना
Infinite loop बनाने का सबसे सीधा तरीका component body के अंदर सीधे state setter function को call करना है। क्योंकि component body हर render पर execute होती है, इसलिए setter हर render पर fire होता है। वह नया state update एक और render का कारण बनता है। यह cycle हमेशा के लिए घूमता रहता है।
गलती कुछ इस तरह दिखती है:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
हर बार जब Counter render होता है, तो यह count को increment करता है। React नए नंबर को दिखाने के लिए फिर से render करता है, फिर से setCount(count + 1) देखता है, और एक बार फिर increment कर देता है। इसका समाधान सीधा है: render के दौरान अपने component के top level पर कभी भी state setter को call न करें। State updates को user events या side effects पर respond करना चाहिए, न कि screen को paint करने की क्रिया पर। उस update को एक event handler में move कर दें:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
Render के दौरान setters को call करने का एकमात्र अपवाद तब है जब आप props से नया state compute कर रहे हों, और तब भी आपको एक अलग pattern का उपयोग करना चाहिए, जैसे कि value को सीधे derive करना या जानबूझकर useEffect का उपयोग करना।
Reference के बजाय Function Call पास करना
एक अन्य सामान्य कारण JSX में एक सूक्ष्म typo है। जब आप एक event handler attach करते हैं, तो आपको function को ही pass करना होता है। यदि आप गलती से JSX में ही function को invoke कर देते हैं, तो यह render cycle के दौरान तुरंत चल जाता है।
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
Parentheses के साथ handleToggle() लिखकर, आप React को वह function नहीं दे रहे हैं जिसे user के click करने पर बाद में call किया जा सके। आप इसे अभी call कर रहे हैं जब React virtual DOM बना रहा है। चूंकि handleToggle state को update करता है, इसलिए component re-render होता है। उस re-render के दौरान, React फिर से handleToggle() देखता है और उसे फिर से call करता है। यह loop कभी खत्म नहीं होता। सही version में parentheses हटा दिए जाते हैं:
return <button onClick={handleToggle}>Toggle</button>;
यदि आपको arguments pass करने की आवश्यकता है, तो call को एक anonymous function में wrap कर दें:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
यह अंतर refactoring के दौरान अनुभवी developers को भी उलझा देता है। उन parentheses पर नज़र रखें।
useEffect Dependency Trap
Effects, side effects जैसे कि data fetch करना, browser APIs के साथ sync करना, या manually DOM को manipulate करने के लिए सही जगह हैं। लेकिन useEffect तब चलता है जब React render को screen पर commit कर देता है। यदि आपका effect state को update करता है, तो React re-render करेगा। सामान्यतः यह ठीक है। यह तब एक loop बन जाता है जब effect हर render के बाद चलता है और हमेशा एक ही state को update करता है।
इस टूटे हुए पैटर्न पर विचार करें:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
चूँकि इसमें कोई dependency array नहीं है, इसलिए यह effect हर एक render के बाद चलता है। यह user को सेट करता है, जिससे एक render ट्रिगर होता है। उस render के बाद, effect फिर से चलता है और user को फिर से सेट करता है। React इस चक्र को पहचान लेता है और error थ्रो कर देता है।
इसका समाधान React को यह बताना है कि effect को वास्तव में कब चलाने की आवश्यकता है, इसके लिए एक उचित dependency array प्रदान करें। यदि effect को केवल mount होने पर एक बार चलाना चाहिए, तो एक empty array पास करें:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
यदि effect किसी prop या state के किसी हिस्से पर निर्भर है, तो array में केवल उसी variable को शामिल करें। हालाँकि, सावधान रहें। ऐसा variable शामिल करना जो हर render पर बदलता है, केवल एक अलग रास्ते से उसी loop को फिर से बना देगा। उदाहरण के लिए, यदि आप dependencies में एक object literal शामिल करते हैं और वह object हर parent render पर फिर से बनाया जाता है, तो effect अंतहीन रूप से चलता रहेगा। ऐसे मामलों में, आपको object creation को component के बाहर ले जाने या उसे memoize करने की आवश्यकता हो सकती है।
व्यावहारिक डिबगिंग चरण
जब आपको यह error मिलता है, तो stack trace बहुत उलझन भरा लग सकता है क्योंकि React पहले ही दर्जनों बार इस चक्र को दोहरा चुका होता है। सबसे पहले trace के ऊपरी हिस्से को पढ़कर यह पता लगाएं कि कौन सा component बार-बार आ रहा है। फिर इन तीन जगहों पर state setters को देखें:
- Component का मुख्य भाग (main body), किसी भी handler या hook के बाहर।
- JSX event attributes जहाँ आपने
handlerके बजायhandler()लिख दिया हो। useEffecthooks जिनमें dependency array की कमी है या जो unstable references पर निर्भर हैं।
जब तक error रुक न जाए, तब तक अस्थायी रूप से प्रत्येक state setter को comment out कर दें। इससे आपको सटीक रूप से पता चल जाएगा कि कौन सा update दोषी है। यदि setter किसी effect के अंदर है, तो खुद से पूछें कि क्या आपको वहां state की वास्तव में आवश्यकता है। कभी-कभी developers effect के अंदर props से local state सेट कर देते हैं, जबकि वे JSX में सीधे prop का उपयोग कर सकते थे।
मुख्य निष्कर्ष
Maximum update depth error कोई रहस्यमयी React bug नहीं है। यह एक safety net है। इसका मतलब है कि आपका component किसी बाहरी signal का इंतज़ार करने के बजाय खुद को re-render करने की कोशिश कर रहा है। Renders को ऐसे events मानने की आदत छोड़ दें जो और अधिक state उत्पन्न करने चाहिए। Renders को state के शुद्ध परिणाम (pure consequences) के रूप में देखें, न कि उसके कारण (causes) के रूप में। State updates को event handlers, callbacks, या सावधानीपूर्वक चुने गए dependencies वाले effects के अंदर रखें, और आप यह error फिर कभी नहीं देखेंगे।
