אתה מרענן את הדף והמסך נשאר ריק. או שאולי לחיצה על כפתור גורמת למאוורר של המעבד שלך לעבוד בשיא המרץ. ואז הקונסול פולט את האזהרה המצמררת: Maximum update depth exceeded. React בלמה בפתאומיות כי הקומפוננטה שלך לכודה בלולאה אינסופית. זו אחת השגיאות הנפוצות ביותר באפליקציות React, והיא נובעת בדרך כלל מאי-הבנה פשוטה לגבי המועד שבו הקוד שלך באמת רץ.
כדי לתקן זאת, עליך להבין את הרגע המדויק שבו רינדור (render) הופך לרינדור מחדש (re-render), ומדוע שינויי state חייבים להישאר מחוץ לנתיב הרינדור.
איך React מרנדרת את הקומפוננטה שלך
React בונה ממשקי משתמש מתוך קומפוננטות. ב-React מודרנית, הקומפוננטות הללו הן פונקציות. בכל פעם ש-React צריכה להציג את הקומפוננטה שלך על המסך, היא פשוט קוראת לפונקציה הזו. בתוך הפונקציה, ניתן להשתמש ב-state כדי לזכור דברים בין רינדורים. ה-state אומר ל-React אילו נתונים שייכים לקומפוננטה, ובאופן קריטי, מתי משהו השתנה וה-UI זקוק לעדכון.
כאשר ה-state משתנה, React מתזמנת רינדור חדש. פונקציית הקומפוננטה רצה שוב, מחזירה JSX חדש, ו-React מעדכנת את ה-DOM בהתאם. בשימוש רגיל, המחזור הזה אינו מזיק. אתה לוחץ על כפתור, event handler מעדכן את ה-state, React מבצעת re-render פעם אחת, והמשתמש רואה את הטקסט או הצבע החדשים.
השגיאה מופיעה כאשר רינדור עצמו מפעיל עדכון state נוסף. עדכון ה-state החדש הזה מפעיל רינדור נוסף, שמפעיל עדכון state נוסף. React מגלה סובלנות למספר עשרות מחזורים, ואז זורקת את שגיאת ה-maximum depth כדי להגן על הדפדפן מפני קיפאון מוחלט.
מבט מהיר על State
לפני שננתח את הלולאה, נזכיר כיצד עובד ה-hook של useState. הוא נותן לך בדיוק שני דברים: משתנה שמחזיק את הערך הנוכחי, ופונקציה לשינוי הערך הזה.
const MessageComponent = () => {
const [message, setMessage] = useState('Welcome');
return <h1>{message}</h1>;
};
כאן, message הוא 'Welcome' ברינדור הראשון. אם תקרא מאוחר יותר ל-setMessage('Goodbye'), React תבחין בשינוי, תקרא ל-MessageComponent שוב, וה-UI יציג כעת "Goodbye". הכל תקין מכיוון ששום דבר בגוף הקומפוננטה עצמו לא קורא ל-setter באופן אוטומטי. הלולאה מתחילה כאשר ה-setter מופעל במהלך שלב הרינדור ללא אירוע חיצוני.
קריאה ל-SetState ישירות בגוף הקומפוננטה
הדרך הישירה ביותר ליצור לולאה אינסופית היא לקרוא לפונקציית setter של ה-state ישירות בתוך גוף הקומפוננטה. מכיוון שגוף הקומפוננטה מבוצע בכל רינדור, ה-setter מופעל בכל רינדור. עדכון ה-state החדש הזה גורם לרינדור נוסף. המחזור מסתובב לנצח.
כך נראית הטעות:
const Counter = () => {
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>;
};
בכל פעם ש-Counter מרונדרת, היא מגדילה את count. React מרנדרת שוב כדי להציג את המספר החדש, מזהה שוב את setCount(count + 1), ומגדילה אותו פעם נוספת. התיקון הוא פשוט: לעולם אל תקרא ל-state setter ברמה הגבוהה (top level) של הקומפוננטה שלך במהלך רינדור. עדכוני state צריכים להגיב לאירועי משתמש או ל-side effects, ולא לפעולת הצגת המסך. העבר את העדכון הזה לתוך event handler במקום:
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
החריג היחיד לקריאה ל-setters במהלך רינדור הוא כאשר אתה מחשב state חדש מתוך props, וגם אז כדאי להשתמש בתבנית שונה, כמו גזירת הערך ישירות או שימוש מכוון ב-useEffect.
העברת קריאה לפונקציה במקום הפניה (Reference)
סיבה נפוצה נוספת היא טעות הקלדה קטנה ב-JSX. כשאתה מצמיד event handler, עליך להעביר את הפונקציה עצמה. אם בטעות מפעיל את הפונקציה ממש שם בתוך ה-JSX, היא תרוץ מיד במהלך מחזור הרינדור.
const Toggle = () => {
const [isOn, setIsOn] = useState(false);
const handleToggle = () => setIsOn(!isOn);
return <button onClick={handleToggle()}>Toggle</button>;
};
על ידי כתיבת handleToggle() עם סוגריים, אינך נותן ל-React פונקציה לקרוא לה מאוחר יותר כשהמשתמש ילחץ. אתה קורא לה ממש עכשיו בזמן ש-React בונה את ה-virtual DOM. מכיוון ש-handleToggle מעדכן את ה-state, הקומפוננטה מבצעת re-render. במהלך ה-re-render הזה, React רואה שוב את handleToggle() וקוראת לה שוב. הלולאה לעולם לא נגמרת. הגרסה הנכונה מסירה את הסוגריים:
return <button onClick={handleToggle}>Toggle</button>;
אם עליך להעביר ארגומנטים, עטוף את הקריאה בפונקציה אנונימית:
return <button onClick={() => handleToggle(true)}>Switch On</button>;
ההבחנה הזו מבלבלת אפילו מפתחים מנוסים במהלך refactoring. שימו לב לסוגריים האלה.
מלכודת התלות של useEffect
Effects הם המקום הנכון עבור side effects כמו שליפת נתונים, סנכרון עם browser APIs, או מניפולציה ידנית של ה-DOM. אך 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.
