Description
Race condition in React state management causes the PixiJS reel canvas to animate with stale outcome data. Every spin visually shows the prior round's reel_matrix while the HUD displays correct win amounts from the current round.
Preconditions
- User is authenticated
- Sufficient balance for at least 2 spins
- At least one previous spin completed (
lastOutcomeexists in state)
Steps to Reproduce
Open the slots game at pyavchik.space/slots
Place a bet and click SPIN
Wait for reels to stop. Note the symbols displayed on the canvas.
Click SPIN again — wait for reels to stop
The symbols now shown on the canvas are from step 2, not the current spin.
Open DevTools → Network tab → inspect the latest /spin response body
Look at outcome.reel_matrix in the JSON response.
Compare the canvas symbols to the API response's reel_matrix
The symbols do NOT match — the canvas shows the prior spin's matrix.
Navigate to Game History → click the latest round → compare the Round Detail reel grid
Round Detail shows the correct (server-side) symbols, confirming the mismatch is frontend-only.
Expected vs Actual Result
Canvas reel symbols match the reel_matrix from the current spin's API response. Displayed symbols, win highlights, and payout amounts are all consistent.
Canvas displays the previous spin's reel_matrix. Every spin result is visually one spin behind the server. Win amounts in the HUD are correct (from API), but displayed symbols belong to the prior round.
Console & Network Logs
Evidence
reel_matrix values.
Root Cause Analysis
The bug is a React state race condition in the spin flow. The useEffect in SlotCanvas depends on both spinning and lastOutcome. When setSpinning(true) is called, the effect fires before the API responds — using the stale outcome from the previous round.
spinning: false → true
spinning changed
Clear stale outcome on spin start
Null out lastOutcome when setSpinning(true) is called. The effect sees lastOutcome === null and exits early. When the fresh API response arrives, the effect fires with correct data — no stale state, no rejected calls.
Diff — frontend/src/store.ts
setSpinning: (v) => set((s) => ({ spinning: v, + // Clear stale outcome when starting a new spin so the SlotCanvas + // effect doesn't re-trigger spinThenStop with the previous result. + lastOutcome: v ? null : s.lastOutcome, lastWinAmount: v ? 0 : (s.pendingWinAmount ?? 0), pendingWinAmount: null, })),
Fixed flow
spinning: true, lastOutcome: null
!lastOutcome is true → early return (no stale animation)