fix(game): move all hooks above early return to fix React error #310
The connecting overlay used an early return before useState/useSoundStore/ useViewportWidth/useMemo/useEffect were called. On the first render with seatPlayers=[] those hooks were skipped; on the next render (state arrived) React tried to call more hooks than before → error #310 crashed all clients. Fix: hoist all hook declarations above the connecting guard so the count is always identical regardless of which branch renders. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,50 +57,27 @@ export function GameTable({
|
||||
const mode = useGameStore((s) => s.mode);
|
||||
const seatPlayers = useGameStore((s) => s.seatPlayers);
|
||||
const { t } = useI18n();
|
||||
|
||||
// While waiting for the first server state broadcast, show a spinner instead
|
||||
// of an empty green felt — prevents the "3 colored dots / stuck table" bug.
|
||||
const connecting = mode === "online" && seatPlayers.length === 0;
|
||||
if (connecting) {
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
|
||||
className="size-14 border-4 border-gold-400/30 border-t-gold-400 rounded-full"
|
||||
/>
|
||||
<p className="gold-text font-bold text-lg">{t("mm.searching")}</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
// All hooks must be declared unconditionally before any early return.
|
||||
const [askFf, setAskFf] = useState(false);
|
||||
|
||||
const sfx = useSoundStore((s) => s.sfx);
|
||||
const music = useSoundStore((s) => s.music);
|
||||
const toggleAll = useSoundStore((s) => s.toggleAll);
|
||||
const muted = !sfx && !music;
|
||||
|
||||
const exit = onExit ?? reset;
|
||||
const vw = useViewportWidth();
|
||||
// Pull the played-card pile inward on narrow screens so it clears the side stacks.
|
||||
const trickScale = vw < 400 ? 0.82 : 1;
|
||||
// Smaller played cards on phones so the center pile stays clear of the side seats.
|
||||
const trickCardSize: "sm" | "md" = vw < 480 ? "sm" : "md";
|
||||
const playHuman = useGameStore((s) => s.playHuman);
|
||||
const chooseTrump = useGameStore((s) => s.chooseTrump);
|
||||
const { phase, players, hakem, trump, turn, currentTrick } = game;
|
||||
|
||||
const legalMovesList = useMemo(
|
||||
() => (phase === "playing" && turn === 0 ? legalMoves(game, 0) : []),
|
||||
[phase, turn, game]
|
||||
);
|
||||
const legalIds = new Set(legalMovesList.map((c) => c.id));
|
||||
|
||||
// Keyboard shortcuts (desktop): 1–9 / 0 play the Nth playable card in hand
|
||||
// order, Space/Enter play the first playable card, M mutes, F forfeits,
|
||||
// Esc/Q quits. A floating hint lists them.
|
||||
const playHuman = useGameStore((s) => s.playHuman);
|
||||
const chooseTrump = useGameStore((s) => s.chooseTrump);
|
||||
// Derived (non-hook) values — safe to compute after all hooks.
|
||||
const muted = !sfx && !music;
|
||||
const exit = onExit ?? reset;
|
||||
const trickScale = vw < 400 ? 0.82 : 1;
|
||||
const trickCardSize: "sm" | "md" = vw < 480 ? "sm" : "md";
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const el = e.target as HTMLElement | null;
|
||||
@@ -133,6 +110,23 @@ export function GameTable({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [phase, turn, hakem, game.players, legalMovesList]);
|
||||
|
||||
// Connecting overlay: all hooks are already called above, so this early
|
||||
// return is safe and won't cause a "more/fewer hooks" error on re-render.
|
||||
if (mode === "online" && seatPlayers.length === 0) {
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
|
||||
className="size-14 border-4 border-gold-400/30 border-t-gold-400 rounded-full"
|
||||
/>
|
||||
<p className="gold-text font-bold text-lg">{t("mm.searching")}</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="persian-pattern relative h-dvh w-full overflow-hidden">
|
||||
{/* Top HUD */}
|
||||
|
||||
Reference in New Issue
Block a user