آپ صفحہ ریفریش کرتے ہیں اور اسکرین خالی رہتی ہے۔ یا شاید کسی بٹن پر کلک کرنے سے آپ کے CPU کا پنکھا پوری رفتار سے چلنے لگتا ہے۔ پھر کنسول میں وہ خوفناک وارننگ آتی ہے: Maximum update depth exceeded۔ React نے بریک لگا دی ہے کیونکہ آپ کا component ایک انفینٹ لوپ (infinite loop) میں پھنس گیا ہے۔ یہ React ایپلی کیشنز میں سب سے عام غلطیوں میں سے ایک ہے، اور اس کی وجہ عام طور پر اس غلط فہمی سے ہوتی ہے کہ آپ کا کوڈ اصل میں کب چلتا ہے۔
اسے ٹھیک کرنے کے لیے، آپ کو یہ سمجھنے کی ضرورت ہے کہ ایک render کب re-render بن جاتا ہے، اور کیوں state کی تبدیلیوں کو render path سے دور رہنا چاہیے۔
React آپ کے Component کو کیسے Render کرتا ہے
React components سے user interfaces بناتا ہے۔ جدید React میں، وہ components فنکشنز ہوتے ہیں۔ ہر بار جب React کو اپنی اسکرین پر آپ کا component دکھانے کی ضرورت ہوتی ہے، تو وہ محض اس فنکشن کو کال کرتا ہے۔ فنکشن کے اندر، آپ renders کے درمیان چیزوں کو یاد رکھنے کے لیے state کا استعمال کر سکتے ہیں۔ State React کو بتاتی ہے کہ کون سا ڈیٹا component کا ہے اور، سب سے اہم بات یہ کہ، کب کچھ تبدیل ہوا ہے اور UI کو اپ ڈیٹ کرنے کی ضرورت ہے۔
جب state تبدیل ہوتی ہے، تو React ایک نیا render شیڈول کرتا ہے۔ Component فنکشن دوبارہ چلتا ہے، نیا JSX واپس کرتا ہے، اور React اسے DOM کے مطابق اپ ڈیٹ کر دیتا ہے۔ عام استعمال میں، یہ سائیکل نقصان دہ نہیں ہے۔ آپ ایک بٹن پر کلک کرتے ہیں، ایک event handler state کو اپ ڈیٹ کرتا ہے، React ایک بار re-render کرتا ہے، اور صارف کو نیا ٹیکسٹ یا رنگ نظر آتا ہے۔
غلطی تب ظاہر ہوتی ہے جب ایک render خود کسی دوسری state update کو ٹرگر کر دے۔ وہ نئی state update ایک اور render کو ٹرگر کرتی ہے، جو کہ ایک اور state update کو ٹرگر کرتی ہے۔ React چند درجن سائیکلز تک اسے برداشت کرتا ہے، پھر براؤزر کو مکمل طور پر فریز ہونے سے بچانے کے لیے maximum depth error دے دیتا ہے۔
State پر ایک نظر
لوپ کا تجزیہ کرنے سے پہلے، یاد کریں کہ useState hook کیسے کام کرتا ہے۔ یہ آپ کو بالکل دو چیزیں دیتا ہے: ایک ویری ایبل جو موجودہ ویلیو کو رکھتا ہے، اور ایک فنکشن جو اس ویلیو کو تبدیل کرتا ہے۔
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
یہاں، پہلے render پر message کی ویلیو 'Welcome' ہے۔ اگر آپ بعد میں setMessage('Goodbye') کال کرتے ہیں، تو React تبدیلی کو نوٹ کرتا ہے، MessageComponent کو دوبارہ کال کرتا ہے، اور اب UI "Goodbye" دکھاتا ہے۔ سب کچھ ٹھیک ہے کیونکہ component کی باڈی میں کوئی بھی چیز خود بخود setter کو کال نہیں کر رہی ہے۔ لوپ تب شروع ہوتا ہے جب setter کسی بیرونی ایونٹ کے بغیر render phase کے دوران چل پڑتا ہے۔
Body میں براہ راست SetState کو کال کرنا
انفینٹ لوپ بنانے کا سب سے براہ راست طریقہ یہ ہے کہ component کی باڈی کے اندر ہی state setter فنکشن کو کال کر دیا جائے۔ چونکہ component کی باڈی ہر render پر چلتی ہے، اس لیے setter ہر render پر چل پڑتا ہے۔ وہ نئی state update ایک اور render کا باعث بنتی ہے۔ یہ سائیکل ہمیشہ کے لیے گھومتا رہتا ہے۔
غلطی کچھ اس طرح نظر آتی ہے:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
ہر بار جب Counter render ہوتا ہے، تو یہ count میں اضافہ کرتا ہے۔ React نئے نمبر کو دکھانے کے لیے دوبارہ render کرتا ہے، پھر سے setCount(count + 1) دیکھتا ہے، اور ایک بار پھر اضافہ کر دیتا ہے۔ اس کا حل سادہ ہے: render کے دوران اپنے component کے top level پر کبھی بھی state setter کو کال نہ کریں۔ State updates کو صارف کے ایونٹس یا side effects کا جواب دینا چاہیے، نہ کہ اسکرین پر تصویر دکھانے (painting) کے عمل کا۔ اس اپ ڈیٹ کو ایک event handler میں منتقل کر دیں:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
render کے دوران setters کو کال کرنے کی واحد استثنا (exception) تب ہے جب آپ props سے نئی state کا حساب لگا رہے ہوں، اور تب بھی آپ کو ایک مختلف پیٹرن استعمال کرنا چاہیے، جیسے کہ ویلیو کو براہ راست حاصل کرنا (deriving) یا جان بوجھ کر useEffect کا استعمال کرنا۔
Reference کے بجائے Function Call پاس کرنا
ایک اور عام وجہ JSX کی ایک معمولی غلطی ہے۔ جب آپ ایک event handler منسلک کرتے ہیں، تو آپ کو خود فنکشن پاس کرنے کی ضرورت ہوتی ہے۔ اگر آپ غلطی سے JSX میں ہی فنکشن کو کال (invoke) کر دیتے ہیں، تو یہ render cycle کے دوران فوری طور پر چل جاتا ہے۔
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
بریکٹس (parentheses) کے ساتھ handleToggle() لکھ کر، آپ React کو وہ فنکشن نہیں دے رہے جسے وہ صارف کے کلک کرنے پر بعد میں کال کر سکے۔ بلکہ آپ اسے اسی وقت کال کر رہے ہیں جب React virtual DOM بنا رہا ہوتا ہے۔ چونکہ handleToggle state کو اپ ڈیٹ کرتا ہے، اس لیے component دوبارہ re-render ہوتا ہے۔ اس re-render کے دوران، React دوبارہ handleToggle() دیکھتا ہے اور اسے دوبارہ کال کرتا ہے۔ یہ لوپ کبھی ختم نہیں ہوتا۔ درست ورژن میں بریکٹس ہٹا دیے جاتے ہیں:
return <button onClick={handleToggle}>Toggle</button>;
اگر آپ کو arguments پاس کرنے کی ضرورت ہے، تو کال کو ایک anonymous function میں لپیٹ دیں:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
یہ فرق ری فیکٹرنگ (refactoring) کے دوران تجربہ کار ڈویلپرز کو بھی الجھا دیتا ہے۔ ان بریکٹس (parentheses) پر نظر رکھیں۔
useEffect Dependency کا جال
Effects، side effects جیسے کہ ڈیٹا حاصل کرنا (fetching data)، browser APIs کے ساتھ سنک (sync) کرنا، یا دستی طور پر DOM کو تبدیل کرنے کے لیے صحیح جگہ ہیں۔ لیکن useEffect اس وقت چلتا ہے جب React render کو اسکرین پر commit کر دیتا ہے۔ اگر آپ کا effect state کو اپ ڈیٹ کرتا ہے، تو React دوبارہ re-render کرے گا۔ عام طور پر یہ ٹھیک ہے، لیکن یہ تب لوپ بن جاتا ہے جب effect ہر render کے بعد چلتا ہے اور ہمیشہ ایک ہی 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.
