तुम्ही पेज रिफ्रेश करता आणि स्क्रीन रिकामीच राहते. किंवा कदाचित एखादे बटण क्लिक केल्यावर तुमच्या CPU फॅनचा वेग प्रचंड वाढतो. मग कन्सोलमध्ये तो भयानक इशारा येतो: Maximum update depth exceeded. React ने ब्रेक लावले आहेत कारण तुमचे component एका अनंत लूपमध्ये (infinite loop) अडकले आहे. हे React ॲप्लिकेशन्समधील सर्वात सामान्य एरर्सपैकी एक आहे आणि सहसा तुमचा कोड नेमका कधी रन होतो याबद्दलच्या साध्या गैरसमजूतमुळे हे घडते.
हे सुधारण्यासाठी, तुम्हाला रेंडर (render) नेमका कधी री-रेंडर (re-render) मध्ये रूपांतरित होतो आणि state बदल (state changes) रेंडर पाथच्या बाहेर का असावे लागतात, हे समजून घेणे आवश्यक आहे.
React तुमचे Component कसे रेंडर करते
React युजर इंटरफेस components पासून बनवते. आधुनिक React मध्ये, हे components फंक्शन्स असतात. जेव्हा जेव्हा React ला तुमचे component स्क्रीनवर दाखवायचे असते, तेव्हा ते फक्त ते फंक्शन कॉल करते. फंक्शनच्या आत, तुम्ही रेंडर्स दरम्यान गोष्टी लक्षात ठेवण्यासाठी state वापरू शकता. State React ला सांगते की कोणता डेटा component चा आहे आणि महत्त्वाचे म्हणजे, जेव्हा काही बदलले आहे आणि UI अपडेट करण्याची गरज आहे.
जेव्हा state बदलते, तेव्हा React नवीन रेंडरचे वेळापत्रक ठरवते. Component फंक्शन पुन्हा रन होते, नवीन JSX रिटर्न करते आणि React त्यानुसार DOM अपडेट करते. सामान्य वापरामध्ये, हे चक्र हानिकारक नसते. तुम्ही बटण क्लिक करता, एक event handler state अपडेट करतो, React एकदा re-render होते आणि वापरकर्त्याला नवीन मजकूर किंवा रंग दिसतो.
जेव्हा एखादे रेंडर स्वतःच दुसरा state update ट्रिगर करते, तेव्हा ही एरर येते. तो नवीन state update पुन्हा दुसरे रेंडर ट्रिगर करते, ज्यामुळे पुन्हा एक state update ट्रिगर होते. React काही डझन सायकलपर्यंत हे सहन करते, आणि त्यानंतर ब्राउझर पूर्णपणे फ्रीझ होण्यापासून वाचवण्यासाठी 'maximum depth error' देते.
State वर एक नजर
लूपचे विश्लेषण करण्यापूर्वी, useState hook कसे काम करते ते आठवून पहा. हे तुम्हाला नेमक्या दोन गोष्टी देते: सध्याची व्हॅल्यू साठवणारा एक व्हेरिएबल आणि ती व्हॅल्यू बदलण्यासाठी एक फंक्शन.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
येथे, पहिल्या रेंडरवर message ची व्हॅल्यू 'Welcome' आहे. जर तुम्ही नंतर setMessage('Goodbye') कॉल केले, तर React तो बदल नोंदवते, MessageComponent पुन्हा कॉल करते आणि आता UI मध्ये "Goodbye" दिसते. सर्व काही ठीक आहे कारण component च्या बॉडीमध्ये काहीही आपोआप setter कॉल करत नाहीये. लूप तेव्हा सुरू होतो जेव्हा कोणताही बाह्य इव्हेंट (external event) नसताना रेंडर फेज दरम्यान setter फायर होतो.
Body मध्ये थेट SetState कॉल करणे
अनंत लूप तयार करण्याचा सर्वात थेट मार्ग म्हणजे component च्या बॉडीमध्ये थेट state setter फंक्शन कॉल करणे. कारण component ची बॉडी प्रत्येक रेंडरवर एक्झिक्युट होते, त्यामुळे प्रत्येक रेंडरवर setter फायर होतो. त्या नवीन state अपडेटमुळे पुन्हा रेंडर होते. हे चक्र कायमस्वरूपी फिरत राहते.
चूक नेमकी कशी दिसते ते येथे पहा:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
प्रत्येक वेळी Counter रेंडर होते, तेव्हा ते count वाढवते. नवीन नंबर दाखवण्यासाठी React पुन्हा रेंडर होते, पुन्हा setCount(count + 1) पाहते आणि पुन्हा एकदा वाढवते. उपाय सोपा आहे: रेंडर दरम्यान तुमच्या component च्या टॉप लेव्हलला कधीही state setter कॉल करू नका. State अपडेट्स हे युजर इव्हेंट्स किंवा side effects ला प्रतिसाद देणारे असावेत, स्क्रीन पेंट करण्याच्या क्रियेला नाही. त्याऐवजी ते अपडेट एखाद्या event handler मध्ये हलवा:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
रेंडर दरम्यान setters कॉल करण्याचे एकमेव अपवाद म्हणजे जेव्हा तुम्ही props मधून नवीन state मोजत असता, आणि तरीही तुम्ही एखादा वेगळा पॅटर्न वापरला पाहिजे, जसे की व्हॅल्यू थेट मिळवणे (deriving) किंवा जाणीवपूर्वक useEffect वापरणे.
Reference ऐवजी Function Call पास करणे
आणखी एक वारंवार होणारे कारण म्हणजे JSX मधील एक छोटीशी चूक (typo). जेव्हा तुम्ही event handler जोडता, तेव्हा तुम्हाला फंक्शन स्वतः पास करावे लागते. जर तुम्ही चुकून JSX मध्येच फंक्शन कॉल केले, तर ते रेंडर सायकल दरम्यान लगेच रन होते.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
कंसासह (parentheses) handleToggle() लिहून, तुम्ही React ला युजरने क्लिक केल्यावर नंतर कॉल करण्यासाठी फंक्शन देत नाही आहात. तुम्ही ते आत्ताच कॉल करत आहात जेव्हा React virtual DOM तयार करत आहे. handleToggle स्टेट अपडेट करत असल्यामुळे, component पुन्हा re-renders होतो. त्या re-render दरम्यान, React पुन्हा handleToggle() पाहते आणि ते पुन्हा कॉल करते. हे लूप कधीच संपत नाही. योग्य आवृत्तीमध्ये कंसाचा वापर केलेला नाही:
return <button onClick={handleToggle}>Toggle</button>;
जर तुम्हाला arguments पास करायचे असतील, तर कॉल एका anonymous function मध्ये गुंडाळा (wrap करा):
return <button onClick={() => handleToggle(true)}>Switch On</button>;
रिफॅक्टरिंग (refactoring) दरम्यान ही सूक्ष्म गोष्ट अनुभवी डेव्हलपर्सनाही गोंधळात टाकू शकते. त्या कंसांकडे (parentheses) लक्ष ठेवा.
useEffect Dependency Trap
डेटा फेच करणे (fetching data), ब्राउझर APIs सोबत सिंक करणे किंवा मॅन्युअली DOM मध्ये बदल करणे यांसारख्या side effects साठी Effects हे योग्य ठिकाण आहे. पण useEffect हे React ने रेंडर स्क्रीनवर कमिट (commit) केल्यानंतर रन होते. जर तुमच्या effect ने state अपडेट केले, तर React पुन्हा re-render होईल. सामान्यतः हे ठीक आहे. पण जेव्हा effect प्रत्येक रेंडरनंतर रन होते आणि नेहमी तीच state अपडेट करते, तेव्हा त्याचे रूपांतर लूपमध्ये होते.
Consider this broken pattern:
const UserProfile = () => {
const [user, setUser] = useState({});
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
});
return <div>{user.name}</div>;
};
Because there is no dependency array, this effect runs after every single render. It sets user, which triggers a render. After that render, the effect runs again and sets user again. React detects the spiral and throws the error.
The fix is to tell React when the effect actually needs to run by supplying a proper dependency array. If the effect should only run once on mount, pass an empty array:
useEffect(() => {
setUser({ name: 'Ada', role: 'Admin' });
}, []);
If the effect depends on a prop or a piece of state, include only that variable in the array. Be careful, though. Including a variable that changes every render will just recreate the same loop through a different door. For example, if you include an object literal in the dependencies and that object is recreated on every parent render, the effect will fire endlessly. In those cases, you may need to move the object creation outside the component or memoize it.
Practical Debugging Steps
When you hit this error, the stack trace can look overwhelming because React has already repeated the cycle dozens of times. Start by reading the top of the trace to find which component is named repeatedly. Then look for state setters in these three places:
- The main body of the component, outside any handler or hook.
- JSX event attributes where you might have written
handler()instead ofhandler. useEffecthooks that lack a dependency array or depend on unstable references.
Temporarily comment out each state setter until the error stops. That tells you exactly which update is the culprit. If the setter is inside an effect, ask yourself whether you even need state there. Sometimes developers set local state from props inside an effect when they could simply use the prop directly in the JSX.
The Real Takeaway
The Maximum update depth error is not a mysterious React bug. It is a safety net. It means your component is trying to re-render itself instead of waiting for an external signal. Break the habit of treating renders as events that should produce more state. Treat renders as pure consequences of state, not as causes. Keep state updates inside event handlers, callbacks, or effects with carefully chosen dependencies, and you will never see this error again.
