Skip to content

Bug: React Maximum update depth exceeded in Ink TUI #2067

@KyleLokK

Description

@KyleLokK

Bug: React "Maximum update depth exceeded" in Ink TUI (useAnimationFrame + useBoxMetrics)

Reasonix version: 0.52.0 – 0.53.1
Node: v25.9.0
OS: Windows 11
Terminal: Windows Terminal / PowerShell 7

Describe the bug

The Ink TUI crashes with Maximum update depth exceeded in multiple scenarios:

  1. When the agent returns multiple edit_file / multi_edit results in a single turn
  2. During AI reasoning streaming
  3. Immediately on startup in some configurations

All crashes produce the same React error but from different hooks.

Stack traces & version matrix

Bug Hook v0.52.0 v0.53.1
A useAnimationFrame ✅ crash confirmed ✅ crash confirmed
B Store dispatch ✅ crash confirmed ❌ not observed (likely refactored)
C useBoxMetrics ❌ not verified ✅ crash confirmed

Bug A — useAnimationFrame:

at dispatchSetState
at onChange                                     ← timer callback
at Timeout.tick                                 ← setInterval handler
at listOnTimeout

Bug B — Store dispatch (v0.52.0 only trace):

at forceStoreRerender
at dispatch
at Object.appendReasoning
at TurnTranslator.flushBuffers

Bug C — useBoxMetrics (v0.53.1 trace):

at dispatchSetState
at commitHookEffectListMount
at commitHookPassiveMountEffects

Root cause & fix

Bug A: useAnimationFrame (packages/ink/src/hooks/use-animation-frame.ts)
ClockContext provides an unstable object per render, which is included in the useEffect dependency array [clock, intervalMs, active]. This causes infinite subscribe/unsubscribe cycles. Additionally, every timer tick calls setTime which triggers a synchronous re-render in React legacy mode.

Fix: wrap clock in a ref, use useRef instead of useState for the time value to avoid re-renders, and remove clock from the deps array.

-  const [time, setTime] = useState(() => clock?.now() ?? 0);
+  const timeRef = useRef(clock?.now() ?? 0);
+  const clockRef = useRef(clock);
+  clockRef.current = clock;
   useEffect(() => {
-    if (!clock || !active) return;
-    let lastUpdate = clock.now();
-    const onChange = () => {
-      const now2 = clock.now();
-      if (now2 - lastUpdate >= intervalMs) {
-        lastUpdate = now2;
-        setTime(now2);
-      }
-    };
-    return clock.subscribe(onChange, true);
-  }, [clock, intervalMs, active]);
-  return [viewportRef, time];
+    if (!clockRef.current || !active) return;
+    const onChange = () => {
+      timeRef.current = clockRef.current.now();
+    };
+    return clockRef.current.subscribe(onChange, true);
+  }, [intervalMs, active]);
+  return [viewportRef, timeRef.current];

Bug B: Custom store dispatch (dist/cli/chunk-GNRKXRRE.js)
dispatch notifies React useSyncExternalStore subscribers synchronously. When called during React's commit phase, this triggers a nested update. Fixed in v0.53.1 by store refactoring.

Bug C: useBoxMetrics (packages/ink/src/hooks/)
useEffect has no dependency array, causing setSize to fire after every render. This creates a render→measure→setState→render loop.

Fix: add [ref.current] dependency array and a guard against duplicate measurements.

   useEffect(() => {
     if (!ref.current) return;
+    if (lastRef.current === ref.current) return;
     lastRef.current = ref.current;
     const { width, height } = measure_element_default(ref.current);
     setSize(prev => prev.width === width && prev.height === height ? prev : { width, height });
-  });
+  }, [ref.current]);

To Reproduce

  1. npx reasonix@0.53.1 (interactive TUI mode)
  2. Ask the agent to perform a task requiring multiple edit_file calls
  3. Observe crash when multiple SEARCH/REPLACE blocks render, or during streaming reasoning

Expected behavior

The UI should render tool results and stream reasoning without crashing.

Additional context

#1956 (v0.52.0, same error) was closed as "Not planned". The issue persists in v0.53.1 with an additional useBoxMetrics crash path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions