From bc5bc4a35737c81eeac28759e025cdb6578d16e9 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 4 Jan 2026 11:03:30 +0100 Subject: [PATCH] Fix React Hooks violation: move useMemo calls before early returns The variationPositionInfo and theoreticalMoves useMemo hooks were called after conditional early returns, violating React's Rules of Hooks. Moved both hooks to be called unconditionally before any early returns. Added null checks inside the hooks to handle missing session/tree safely. --- .../OpeningTrainer/OpeningTrainer.tsx | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/components/OpeningTrainer/OpeningTrainer.tsx b/src/components/OpeningTrainer/OpeningTrainer.tsx index df11071..61efdea 100644 --- a/src/components/OpeningTrainer/OpeningTrainer.tsx +++ b/src/components/OpeningTrainer/OpeningTrainer.tsx @@ -276,6 +276,44 @@ export default function OpeningTrainer({ setLastTutorMessageMoveIndex(moveCount); }; + // ============================================================================ + // Family mode hooks - MUST be called before any early returns + // ============================================================================ + + // Family mode: compute current position info from variation tree + // IMPORTANT: Always call useMemo, even if not in family mode (React Hooks rule) + const variationPositionInfo = useMemo(() => { + if (!isFamilyMode || !variationTree || !session) { + return null; + } + const positionDesc = describeCurrentPosition(variationTree, session.moveHistory); + const currentVariations = identifyCurrentVariation(variationTree, session.moveHistory); + const possibleMoves = getAllPossibleNextMoves(variationTree, session.moveHistory); + + return { + ...positionDesc, + currentVariations, + possibleMoves, + isInAnyVariation: positionDesc.matchingCount > 0, + }; + }, [isFamilyMode, variationTree, session]); + + // Get all possible next moves (for display and tutor context) + const theoreticalMoves = useMemo(() => { + if (isFamilyMode && variationPositionInfo) { + return variationPositionInfo.nextMoves; + } + // Single variation mode + if (!session) return []; + const moves = parseMoveSequence(opening.moves); + const nextMove = moves[session.moveHistory.length]; + return nextMove ? [nextMove] : []; + }, [isFamilyMode, variationPositionInfo, opening.moves, session]); + + // ============================================================================ + // Early returns for loading/error states + // ============================================================================ + // Session recovery dialog if (showRecoveryDialog && existingSession) { return ( @@ -384,33 +422,7 @@ export default function OpeningTrainer({ const lastUserMove = userMoves.length > 0 ? userMoves[userMoves.length - 1] : null; const lastTutorMove = tutorMoves.length > 0 ? tutorMoves[tutorMoves.length - 1] : null; - // Family mode: compute current position info from variation tree - const variationPositionInfo = useMemo(() => { - if (!isFamilyMode || !variationTree) { - return null; - } - const positionDesc = describeCurrentPosition(variationTree, session.moveHistory); - const currentVariations = identifyCurrentVariation(variationTree, session.moveHistory); - const possibleMoves = getAllPossibleNextMoves(variationTree, session.moveHistory); - - return { - ...positionDesc, - currentVariations, - possibleMoves, - isInAnyVariation: positionDesc.matchingCount > 0, - }; - }, [isFamilyMode, variationTree, session.moveHistory]); - - // Get all possible next moves (for display and tutor context) - const theoreticalMoves = useMemo(() => { - if (isFamilyMode && variationPositionInfo) { - return variationPositionInfo.nextMoves; - } - // Single variation mode - const moves = parseMoveSequence(opening.moves); - const nextMove = moves[session.moveHistory.length]; - return nextMove ? [nextMove] : []; - }, [isFamilyMode, variationPositionInfo, opening.moves, session.moveHistory.length]); + // Note: variationPositionInfo and theoreticalMoves are computed above (before early returns) const openingPracticeMode = { openingName: opening.name,