BUG #209: Clicking "Climb the Spire" crashes with React error #185 (Maximum update depth exceeded) #250

Closed
opened 2026-06-01 15:04:34 +02:00 by Anexim · 1 comment
Owner

Bug: React error #185 when entering Spire mode

Severity: High — game-breaking crash on user action
Reported: User report + e2e test at e2e/playtest.spec.ts:144 labels this "KNOWN BUG #209"

Error

Something went wrong: Minified React error #185
→ Maximum update depth exceeded. This can happen when a component repeatedly
  calls setState inside componentWillUpdate or componentDidUpdate.

Steps to Reproduce

  1. Start the game (fresh or existing save)
  2. Click the "Climb the Spire" button (or navigate to Spire tab and click enter)
  3. Game crashes immediately or within seconds of entering spire mode

Affected Components (traced from enterSpireMode → render path)

File Role
src/app/components/LeftPanel.tsx:86 Button triggers enterSpireMode()
src/lib/game/stores/combatStore.ts:210 enterSpireMode() sets spireMode: true, currentAction: 'climb', currentFloor: 1
src/app/page.tsx:189-196 spireMode === true renders <SpireCombatPage /> inside ErrorBoundary + Suspense
src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx Primary suspect — mount + render loop
src/lib/game/stores/gameStore.ts:tick Game tick (setInterval 200ms) processes combat when currentAction === 'climb'

Root Cause Analysis

The most likely trigger is an infinite re-render loop in SpireCombatPage caused by the interaction of:

  1. useEffect([currentFloor, totalRooms, setCurrentRoom]) at line ~128 of SpireCombatPage.tsx:

    • Calls setRoomsCleared(0) (local state) + setCurrentRoom(newRoom) (Zustand combat store)
    • Runs whenever currentFloor or totalRooms changes
  2. Game tick (useGameLoop, 200ms interval):

    • When currentAction === 'climb', calls processCombatTick()onFloorCleared()currentFloor advances
    • Writes combat: { currentFloor: newFloor, ... } back to the store via applyTickWrites
  3. useShallow selector subscribes to currentRoom:

    • setCurrentRoom in the effect changes currentRoom → component re-renders
    • If the tick also advanced currentFloor in the same render window, the effect fires again

Loop path: render → useEffect → setCurrentRoom → store update → re-render → currentFloor changed → useEffect → setCurrentRoom → ...

React 19's automatic batching should prevent this in many cases, making the bug intermittent / timing-dependent — consistent with the e2e test showing "No crash detected - may be fixed" on fresh state but users still experiencing it in production.

Additional Suspects

  • enterSpireMode() uses generateSpireFloorState(1, 0, 1) — hardcodes totalRooms: 1 which may conflict with SpireCombatPage's own totalRooms calculation via useMemo (which would compute a different value like 5-7 for floor 1)
  • useSpireStats() calls computeDisciplineEffects() on every render (no memoization), producing new object references each render — though this returns primitives so shouldn't directly cause a loop
  • No cleanup or guard in SpireCombatPage's useEffect to prevent redundant setCurrentRoom calls when the room hasn't meaningfully changed

E2E Test Coverage

e2e/playtest.spec.ts:144 — "KNOWN BUG #209: Climb the Spire should not crash with React error #185"

  • Test currently passes (no crash detected on fresh game state)
  • Bug may be state-dependent (requires specific localStorage conditions)

Suggested Fix Approach (not implementing — investigation only)

  • Add a guard in the useEffect to skip setCurrentRoom if the generated room is structurally equal to the current one
  • Stabilize seededRandom reference (currently a new function on every currentFloor change — by design — but the downstream totalRooms memo should be sufficient)
  • Consider whether the useEffect is needed at all, since enterSpireMode() already sets currentRoom in the store
  • Add react-hooks/exhaustive-deps verification to ensure no missing/extra dependencies
## Bug: React error #185 when entering Spire mode **Severity:** High — game-breaking crash on user action **Reported:** User report + e2e test at `e2e/playtest.spec.ts:144` labels this "KNOWN BUG #209" ### Error ``` Something went wrong: Minified React error #185 → Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. ``` ### Steps to Reproduce 1. Start the game (fresh or existing save) 2. Click the "Climb the Spire" button (or navigate to Spire tab and click enter) 3. Game crashes immediately or within seconds of entering spire mode ### Affected Components (traced from `enterSpireMode` → render path) | File | Role | |---|---| | `src/app/components/LeftPanel.tsx:86` | Button triggers `enterSpireMode()` | | `src/lib/game/stores/combatStore.ts:210` | `enterSpireMode()` sets `spireMode: true`, `currentAction: 'climb'`, `currentFloor: 1` | | `src/app/page.tsx:189-196` | `spireMode === true` renders `<SpireCombatPage />` inside `ErrorBoundary` + `Suspense` | | `src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx` | **Primary suspect** — mount + render loop | | `src/lib/game/stores/gameStore.ts:tick` | Game tick (`setInterval` 200ms) processes combat when `currentAction === 'climb'` | ### Root Cause Analysis The most likely trigger is an **infinite re-render loop** in `SpireCombatPage` caused by the interaction of: 1. **`useEffect([currentFloor, totalRooms, setCurrentRoom])`** at line ~128 of `SpireCombatPage.tsx`: - Calls `setRoomsCleared(0)` (local state) + `setCurrentRoom(newRoom)` (Zustand combat store) - Runs whenever `currentFloor` or `totalRooms` changes 2. **Game tick** (`useGameLoop`, 200ms interval): - When `currentAction === 'climb'`, calls `processCombatTick()` → `onFloorCleared()` → `currentFloor` advances - Writes `combat: { currentFloor: newFloor, ... }` back to the store via `applyTickWrites` 3. **`useShallow` selector** subscribes to `currentRoom`: - `setCurrentRoom` in the effect changes `currentRoom` → component re-renders - If the tick also advanced `currentFloor` in the same render window, the effect fires again **Loop path:** `render → useEffect → setCurrentRoom → store update → re-render → currentFloor changed → useEffect → setCurrentRoom → ...` React 19's automatic batching should prevent this in many cases, making the bug **intermittent / timing-dependent** — consistent with the e2e test showing "No crash detected - may be fixed" on fresh state but users still experiencing it in production. ### Additional Suspects - `enterSpireMode()` uses `generateSpireFloorState(1, 0, 1)` — hardcodes `totalRooms: 1` which may conflict with `SpireCombatPage`'s own `totalRooms` calculation via `useMemo` (which would compute a different value like 5-7 for floor 1) - `useSpireStats()` calls `computeDisciplineEffects()` on every render (no memoization), producing new object references each render — though this returns primitives so shouldn't directly cause a loop - No `cleanup` or guard in `SpireCombatPage`'s `useEffect` to prevent redundant `setCurrentRoom` calls when the room hasn't meaningfully changed ### E2E Test Coverage `e2e/playtest.spec.ts:144` — "KNOWN BUG #209: Climb the Spire should not crash with React error #185" - Test currently passes (no crash detected on fresh game state) - Bug may be state-dependent (requires specific localStorage conditions) ### Suggested Fix Approach (not implementing — investigation only) - Add a guard in the `useEffect` to skip `setCurrentRoom` if the generated room is structurally equal to the current one - Stabilize `seededRandom` reference (currently a new function on every `currentFloor` change — by design — but the downstream `totalRooms` memo should be sufficient) - Consider whether the `useEffect` is needed at all, since `enterSpireMode()` already sets `currentRoom` in the store - Add `react-hooks/exhaustive-deps` verification to ensure no missing/extra dependencies
Anexim added the ai:todo label 2026-06-01 15:04:34 +02:00
n8n-gitea was assigned by Anexim 2026-06-01 15:04:34 +02:00
Author
Owner

Fixed: Added a useRef guard in SpireCombatPage's useEffect to prevent infinite re-render loop. The effect now tracks the last currentFloor:totalRooms combination it generated a room for, and skips redundant calls to setCurrentRoom. This breaks the loop where: effect fires → setCurrentRoom → store update → re-render → tick advances currentFloor → effect fires → ... All existing tests pass (46 tests across tick-integration, combat-actions, SpireSummaryTab suites).

Fixed: Added a useRef guard in SpireCombatPage's useEffect to prevent infinite re-render loop. The effect now tracks the last `currentFloor:totalRooms` combination it generated a room for, and skips redundant calls to `setCurrentRoom`. This breaks the loop where: effect fires → setCurrentRoom → store update → re-render → tick advances currentFloor → effect fires → ... All existing tests pass (46 tests across tick-integration, combat-actions, SpireSummaryTab suites).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Anexim/Mana-Loop#250