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:
- When the agent returns multiple
edit_file / multi_edit results in a single turn
- During AI reasoning streaming
- 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
npx reasonix@0.53.1 (interactive TUI mode)
- Ask the agent to perform a task requiring multiple
edit_file calls
- 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.
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 exceededin multiple scenarios:edit_file/multi_editresults in a single turnAll crashes produce the same React error but from different hooks.
Stack traces & version matrix
useAnimationFramedispatchuseBoxMetricsBug A — useAnimationFrame:
Bug B — Store dispatch (v0.52.0 only trace):
Bug C — useBoxMetrics (v0.53.1 trace):
Root cause & fix
Bug A:
useAnimationFrame(packages/ink/src/hooks/use-animation-frame.ts)ClockContextprovides an unstable object per render, which is included in theuseEffectdependency array[clock, intervalMs, active]. This causes infinite subscribe/unsubscribe cycles. Additionally, every timer tick callssetTimewhich triggers a synchronous re-render in React legacy mode.Fix: wrap
clockin a ref, useuseRefinstead ofuseStatefor the time value to avoid re-renders, and removeclockfrom the deps array.Bug B: Custom store dispatch (dist/cli/chunk-GNRKXRRE.js)
dispatchnotifies ReactuseSyncExternalStoresubscribers 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/)useEffecthas no dependency array, causingsetSizeto 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
npx reasonix@0.53.1(interactive TUI mode)edit_filecallsExpected 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
useBoxMetricscrash path.