diff --git a/web/src/components/Table.tsx b/web/src/components/Table.tsx
index 5e1e524..0988a11 100644
--- a/web/src/components/Table.tsx
+++ b/web/src/components/Table.tsx
@@ -14,22 +14,12 @@ import { EventLog, battleLogLines } from './EventLog'
export function Table({ session, onLeave }: { session: Session; onLeave: () => void }) {
const { view, error, connected, send } = useGame(session)
- if (!view) {
- return (
-
- {connected ? 'Loading game…' : 'Connecting…'}
-
- )
- }
-
- const you = view.players[view.youSeat]
- const opponents = view.players.filter((p) => p.seat !== view.youSeat)
-
// Battle replay step lives here (not inside BattlePhase) so the event log,
// a sibling, can render the battle narration up to the same step. Deriving
// the effective step from the current battle round resets it to 0 whenever a
- // new battle arrives, without a separate effect.
- const battleRound = view.battle?.round ?? -1
+ // new battle arrives, without a separate effect. These hooks must run on
+ // every render (before any early return) to satisfy the rules of hooks.
+ const battleRound = view?.battle?.round ?? -1
const [stepState, setStepState] = useState<{ round: number; step: number }>({
round: -1,
step: 0,
@@ -44,12 +34,23 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
const battleLines = useMemo(
() =>
- view.phase === 'battle' && view.battle?.events
+ view?.phase === 'battle' && view.battle?.events
? battleLogLines(view.battle.events, step)
: undefined,
- [view.phase, view.battle, step],
+ [view, step],
)
+ if (!view) {
+ return (
+
+ {connected ? 'Loading game…' : 'Connecting…'}
+
+ )
+ }
+
+ const you = view.players[view.youSeat]
+ const opponents = view.players.filter((p) => p.seat !== view.youSeat)
+
return (