Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:144labels this "KNOWN BUG #209"Error
Steps to Reproduce
Affected Components (traced from
enterSpireMode→ render path)src/app/components/LeftPanel.tsx:86enterSpireMode()src/lib/game/stores/combatStore.ts:210enterSpireMode()setsspireMode: true,currentAction: 'climb',currentFloor: 1src/app/page.tsx:189-196spireMode === truerenders<SpireCombatPage />insideErrorBoundary+Suspensesrc/components/game/tabs/SpireCombatPage/SpireCombatPage.tsxsrc/lib/game/stores/gameStore.ts:ticksetInterval200ms) processes combat whencurrentAction === 'climb'Root Cause Analysis
The most likely trigger is an infinite re-render loop in
SpireCombatPagecaused by the interaction of:useEffect([currentFloor, totalRooms, setCurrentRoom])at line ~128 ofSpireCombatPage.tsx:setRoomsCleared(0)(local state) +setCurrentRoom(newRoom)(Zustand combat store)currentFloorortotalRoomschangesGame tick (
useGameLoop, 200ms interval):currentAction === 'climb', callsprocessCombatTick()→onFloorCleared()→currentFlooradvancescombat: { currentFloor: newFloor, ... }back to the store viaapplyTickWritesuseShallowselector subscribes tocurrentRoom:setCurrentRoomin the effect changescurrentRoom→ component re-renderscurrentFloorin the same render window, the effect fires againLoop 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()usesgenerateSpireFloorState(1, 0, 1)— hardcodestotalRooms: 1which may conflict withSpireCombatPage's owntotalRoomscalculation viauseMemo(which would compute a different value like 5-7 for floor 1)useSpireStats()callscomputeDisciplineEffects()on every render (no memoization), producing new object references each render — though this returns primitives so shouldn't directly cause a loopcleanupor guard inSpireCombatPage'suseEffectto prevent redundantsetCurrentRoomcalls when the room hasn't meaningfully changedE2E Test Coverage
e2e/playtest.spec.ts:144— "KNOWN BUG #209: Climb the Spire should not crash with React error #185"Suggested Fix Approach (not implementing — investigation only)
useEffectto skipsetCurrentRoomif the generated room is structurally equal to the current oneseededRandomreference (currently a new function on everycurrentFloorchange — by design — but the downstreamtotalRoomsmemo should be sufficient)useEffectis needed at all, sinceenterSpireMode()already setscurrentRoomin the storereact-hooks/exhaustive-depsverification to ensure no missing/extra dependenciesFixed: Added a useRef guard in SpireCombatPage's useEffect to prevent infinite re-render loop. The effect now tracks the last
currentFloor:totalRoomscombination it generated a room for, and skips redundant calls tosetCurrentRoom. 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).