React Guide - RumenDamyanov/js-chess GitHub Wiki

React Guide

Learn how to build chess applications using React with the JS Chess project, leveraging modern React patterns including hooks, context, and component composition.

Overview

The React implementation demonstrates how to use the popular component-based library to build interactive chess applications. It showcases modern React patterns including hooks, context API, and functional components with TypeScript support.

Project Structure

react/
├── package.json            # Dependencies and scripts
├── vite.config.js          # Vite build configuration
├── index.html              # HTML entry point
├── src/
│   ├── main.jsx            # Application entry point
│   ├── App.jsx             # Root component
│   ├── components/         # React components
│   │   ├── ChessBoard.jsx  # Chess board component
│   │   ├── GameControls.jsx # Game control buttons
│   │   ├── GameStatus.jsx  # Status display
│   │   ├── MoveHistory.jsx # Move history component
│   │   ├── SettingsModal.jsx # Settings dialog
│   │   └── LoadingSpinner.jsx # Loading component
│   ├── hooks/              # Custom React hooks
│   │   ├── useChessGame.js # Game logic hook
│   │   ├── useWebSocket.js # WebSocket hook
│   │   ├── useSettings.js  # Settings hook
│   │   └── useLocalStorage.js # Storage hook
│   ├── context/            # React context providers
│   │   ├── GameContext.jsx # Game state context
│   │   └── SettingsContext.jsx # Settings context
│   ├── services/           # API services
│   │   └── chessApi.js     # API client
│   ├── utils/              # Utility functions
│   │   ├── chessHelpers.js # Chess logic helpers
│   │   └── constants.js    # Application constants
│   ├── assets/             # Static assets
│   └── styles/             # Component styles
│       ├── index.css       # Global styles
│       └── components/     # Component-specific styles
└── public/                 # Public assets
    └── pieces/             # Chess piece images

Getting Started

1. Setup Development Environment

# Clone the repository
git clone --recursive https://github.com/RumenDamyanov/js-chess.git
cd js-chess

# Start the React application
make run-react

# Or manually
cd react
npm install
npm run dev

2. Open in Browser

Visit http://localhost:3004 to see the React chess application in action.

Core Implementation

Main Application Setup

The main.jsx file bootstraps the React application:

// main.jsx - React application entry point
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { GameProvider } from './context/GameContext'
import { SettingsProvider } from './context/SettingsContext'
import './styles/index.css'

// Error boundary for development
import ErrorBoundary from './components/ErrorBoundary'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ErrorBoundary>
      <SettingsProvider>
        <GameProvider>
          <App />
        </GameProvider>
      </SettingsProvider>
    </ErrorBoundary>
  </React.StrictMode>
)

Root Component

The App.jsx component serves as the application shell:

// App.jsx - Root application component
import React, { useEffect, useState } from 'react'
import { useGame } from './context/GameContext'
import { useSettings } from './context/SettingsContext'
import ChessBoard from './components/ChessBoard'
import GameControls from './components/GameControls'
import GameStatus from './components/GameStatus'
import MoveHistory from './components/MoveHistory'
import SettingsModal from './components/SettingsModal'
import LoadingSpinner from './components/LoadingSpinner'
import ErrorMessage from './components/ErrorMessage'

function App() {
  const [showSettings, setShowSettings] = useState(false)
  const [error, setError] = useState(null)

  const {
    gameState,
    selectedSquare,
    validMoves,
    moveHistory,
    isPlayerTurn,
    isLoading,
    createNewGame,
    makeMove,
    undoLastMove,
    getHint,
    jumpToMove,
    selectSquare,
    clearSelection
  } = useGame()

  const { settings, updateSettings } = useSettings()

  useEffect(() => {
    // Initialize game on component mount
    const initializeGame = async () => {
      try {
        await createNewGame()
      } catch (err) {
        setError('Failed to initialize game')
        console.error('Initialization error:', err)
      }
    }

    initializeGame()
  }, [createNewGame])

  const handleSquareClick = async (position) => {
    if (!isPlayerTurn || isLoading) return

    try {
      if (selectedSquare === position) {
        clearSelection()
        return
      }

      if (selectedSquare && validMoves.includes(position)) {
        await makeMove(selectedSquare, position)
        return
      }

      selectSquare(position)
    } catch (err) {
      setError('Failed to make move')
      console.error('Move error:', err)
    }
  }

  const handlePieceDrag = async (from, to) => {
    if (!isPlayerTurn || isLoading) return

    try {
      if (validMoves.includes(to)) {
        await makeMove(from, to)
      }
    } catch (err) {
      setError('Failed to make move')
      console.error('Drag move error:', err)
    }
  }

  const handleNewGame = async () => {
    try {
      await createNewGame()
      setError(null)
    } catch (err) {
      setError('Failed to create new game')
      console.error('New game error:', err)
    }
  }

  const handleUndo = async () => {
    try {
      await undoLastMove()
      setError(null)
    } catch (err) {
      setError('Failed to undo move')
      console.error('Undo error:', err)
    }
  }

  const handleGetHint = async () => {
    try {
      const hint = await getHint()
      // Display hint to user
      console.log('Hint:', hint)
    } catch (err) {
      setError('Failed to get hint')
      console.error('Hint error:', err)
    }
  }

  const handleSettingsSave = (newSettings) => {
    updateSettings(newSettings)
    setShowSettings(false)
  }

  const closeError = () => {
    setError(null)
  }

  return (
    <div className="app">
      <header className="app-header">
        <nav className="app-nav">
          <div className="nav-brand">
            <h1>JS Chess</h1>
            <span className="framework-badge">React</span>
          </div>
          <div className="nav-links">
            <a href="/">Home</a>
            <a href="/vanilla-js">Vanilla JS</a>
            <a href="/jquery">jQuery</a>
            <a href="/vue">Vue.js</a>
            <a href="/react" className="active">React</a>
            <a href="/angular">Angular</a>
          </div>
        </nav>
      </header>

      <main className="container">
        {isLoading && <LoadingSpinner />}

        <div className="game-container">
          <ChessBoard
            board={gameState.board}
            selectedSquare={selectedSquare}
            validMoves={validMoves}
            lastMove={gameState.lastMove}
            onSquareClick={handleSquareClick}
            onPieceDrag={handlePieceDrag}
            theme={settings.boardTheme}
            showCoordinates={settings.showCoordinates}
          />

          <div className="game-sidebar">
            <GameControls
              isPlayerTurn={isPlayerTurn}
              canUndo={moveHistory.length > 0}
              isLoading={isLoading}
              onNewGame={handleNewGame}
              onUndo={handleUndo}
              onGetHint={handleGetHint}
              onShowSettings={() => setShowSettings(true)}
            />

            <GameStatus
              gameState={gameState}
              isPlayerTurn={isPlayerTurn}
            />

            <MoveHistory
              moves={moveHistory}
              onJumpToMove={jumpToMove}
            />
          </div>
        </div>
      </main>

      {showSettings && (
        <SettingsModal
          settings={settings}
          onSave={handleSettingsSave}
          onClose={() => setShowSettings(false)}
        />
      )}

      {error && (
        <ErrorMessage
          message={error}
          onClose={closeError}
        />
      )}
    </div>
  )
}

export default App

Game Context Provider

The GameContext.jsx manages global game state:

// context/GameContext.jsx
import React, { createContext, useContext, useReducer, useCallback } from 'react'
import { chessApi } from '../services/chessApi'
import { gameReducer, initialGameState } from '../reducers/gameReducer'

const GameContext = createContext()

export const useGame = () => {
  const context = useContext(GameContext)
  if (!context) {
    throw new Error('useGame must be used within a GameProvider')
  }
  return context
}

export const GameProvider = ({ children }) => {
  const [state, dispatch] = useReducer(gameReducer, initialGameState)

  const createNewGame = useCallback(async () => {
    dispatch({ type: 'SET_LOADING', payload: true })

    try {
      const response = await chessApi.createGame({
        ai_enabled: true,
        difficulty: 'medium'
      })

      dispatch({ type: 'GAME_CREATED', payload: response })
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message })
      throw error
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false })
    }
  }, [])

  const makeMove = useCallback(async (from, to) => {
    if (!state.isPlayerTurn || !state.gameState.id) return

    dispatch({ type: 'SET_PLAYER_TURN', payload: false })

    try {
      const response = await chessApi.makeMove(state.gameState.id, from, to)

      if (response.success) {
        dispatch({ type: 'MOVE_MADE', payload: response })

        // Clear selection
        dispatch({ type: 'CLEAR_SELECTION' })

        // Check for game end
        if (!response.gameState.gameOver) {
          // Get AI move
          await makeAIMove()
        }
      }
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message })
      dispatch({ type: 'SET_PLAYER_TURN', payload: true })
      throw error
    }
  }, [state.isPlayerTurn, state.gameState.id])

  const makeAIMove = useCallback(async () => {
    try {
      const response = await chessApi.getAIMove(state.gameState.id)

      if (response.success) {
        dispatch({ type: 'AI_MOVE_MADE', payload: response })
        dispatch({ type: 'SET_PLAYER_TURN', payload: true })
      }
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message })
      dispatch({ type: 'SET_PLAYER_TURN', payload: true })
    }
  }, [state.gameState.id])

  const undoLastMove = useCallback(async () => {
    if (state.moveHistory.length === 0) return

    dispatch({ type: 'SET_LOADING', payload: true })

    try {
      const response = await chessApi.undoMove(state.gameState.id)

      if (response.success) {
        dispatch({ type: 'MOVE_UNDONE', payload: response })
      }
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message })
      throw error
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false })
    }
  }, [state.gameState.id, state.moveHistory.length])

  const getHint = useCallback(async () => {
    try {
      const response = await chessApi.getHint(state.gameState.id)
      return response
    } catch (error) {
      dispatch({ type: 'SET_ERROR', payload: error.message })
      throw error
    }
  }, [state.gameState.id])

  const selectSquare = useCallback((position) => {
    dispatch({ type: 'SELECT_SQUARE', payload: position })
  }, [])

  const clearSelection = useCallback(() => {
    dispatch({ type: 'CLEAR_SELECTION' })
  }, [])

  const jumpToMove = useCallback((moveIndex) => {
    dispatch({ type: 'JUMP_TO_MOVE', payload: moveIndex })
  }, [])

  const value = {
    ...state,
    createNewGame,
    makeMove,
    undoLastMove,
    getHint,
    selectSquare,
    clearSelection,
    jumpToMove
  }

  return (
    <GameContext.Provider value={value}>
      {children}
    </GameContext.Provider>
  )
}

Game Reducer

The gameReducer.js manages state transitions:

// reducers/gameReducer.js
export const initialGameState = {
  gameState: {
    id: null,
    board: '',
    status: 'ready',
    activeColor: 'white',
    inCheck: false,
    gameOver: false,
    winner: null,
    lastMove: null
  },
  selectedSquare: null,
  validMoves: [],
  moveHistory: [],
  currentMoveIndex: -1,
  isPlayerTurn: true,
  isLoading: false,
  error: null
}

export const gameReducer = (state, action) => {
  switch (action.type) {
    case 'SET_LOADING':
      return {
        ...state,
        isLoading: action.payload
      }

    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload
      }

    case 'CLEAR_ERROR':
      return {
        ...state,
        error: null
      }

    case 'GAME_CREATED':
      return {
        ...state,
        gameState: action.payload,
        selectedSquare: null,
        validMoves: [],
        moveHistory: [],
        currentMoveIndex: -1,
        isPlayerTurn: true,
        error: null
      }

    case 'SELECT_SQUARE':
      const validMoves = getValidMovesForPosition(action.payload, state.gameState)
      return {
        ...state,
        selectedSquare: action.payload,
        validMoves
      }

    case 'CLEAR_SELECTION':
      return {
        ...state,
        selectedSquare: null,
        validMoves: []
      }

    case 'MOVE_MADE':
      return {
        ...state,
        gameState: action.payload.gameState,
        moveHistory: [...state.moveHistory, action.payload.move],
        currentMoveIndex: state.moveHistory.length,
        selectedSquare: null,
        validMoves: []
      }

    case 'AI_MOVE_MADE':
      return {
        ...state,
        gameState: action.payload.gameState,
        moveHistory: [...state.moveHistory, action.payload.move],
        currentMoveIndex: state.moveHistory.length
      }

    case 'MOVE_UNDONE':
      return {
        ...state,
        gameState: action.payload.gameState,
        moveHistory: state.moveHistory.slice(0, -2), // Remove last 2 moves
        currentMoveIndex: Math.max(-1, state.currentMoveIndex - 2),
        selectedSquare: null,
        validMoves: [],
        isPlayerTurn: true
      }

    case 'SET_PLAYER_TURN':
      return {
        ...state,
        isPlayerTurn: action.payload
      }

    case 'JUMP_TO_MOVE':
      return {
        ...state,
        currentMoveIndex: action.payload
      }

    default:
      return state
  }
}

// Helper function to get valid moves for a position
const getValidMovesForPosition = (position, gameState) => {
  // This should come from the API in a real implementation
  // For now, return empty array
  return []
}

Chess Board Component

The ChessBoard.jsx component handles board rendering and interactions:

// components/ChessBoard.jsx
import React, { useMemo, useCallback } from 'react'
import { useDrag, useDrop, DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import Square from './Square'
import Piece from './Piece'
import '../styles/components/ChessBoard.css'

const ChessBoard = ({
  board,
  selectedSquare,
  validMoves,
  lastMove,
  onSquareClick,
  onPieceDrag,
  theme = 'classic',
  showCoordinates = true
}) => {
  const squares = useMemo(() => {
    const squareArray = []

    for (let rank = 8; rank >= 1; rank--) {
      for (let file = 0; file < 8; file++) {
        const fileChar = String.fromCharCode(97 + file) // a-h
        const position = fileChar + rank

        squareArray.push({
          position,
          file: fileChar,
          rank,
          isLight: (rank + file) % 2 === 1,
          piece: getPieceAt(position, board),
          isSelected: selectedSquare === position,
          isValidMove: validMoves.includes(position),
          isLastMove: lastMove && (lastMove.from === position || lastMove.to === position),
          showFileLabel: rank === 1 && showCoordinates,
          showRankLabel: file === 0 && showCoordinates
        })
      }
    }

    return squareArray
  }, [board, selectedSquare, validMoves, lastMove, showCoordinates])

  const handleSquareClick = useCallback((position) => {
    onSquareClick(position)
  }, [onSquareClick])

  const handlePieceDrop = useCallback((from, to) => {
    onPieceDrag(from, to)
  }, [onPieceDrag])

  return (
    <DndProvider backend={HTML5Backend}>
      <div className={`chess-board theme-${theme}`}>
        {squares.map((square) => (
          <Square
            key={square.position}
            {...square}
            onClick={handleSquareClick}
            onPieceDrop={handlePieceDrop}
          />
        ))}
      </div>
    </DndProvider>
  )
}

// Helper function to get piece at position
const getPieceAt = (position, board) => {
  if (!board) return null

  // Parse board string and find piece at position
  // This implementation depends on the board format from your API
  return parseBoardString(board, position)
}

const parseBoardString = (boardString, position) => {
  // Implementation depends on board format from API
  // This is a placeholder
  return null
}

export default ChessBoard

Square Component

// components/Square.jsx
import React from 'react'
import { useDrop } from 'react-dnd'
import Piece from './Piece'

const Square = ({
  position,
  file,
  rank,
  isLight,
  piece,
  isSelected,
  isValidMove,
  isLastMove,
  showFileLabel,
  showRankLabel,
  onClick,
  onPieceDrop
}) => {
  const [{ isOver }, drop] = useDrop({
    accept: 'piece',
    drop: (item) => {
      if (item.position !== position) {
        onPieceDrop(item.position, position)
      }
    },
    collect: (monitor) => ({
      isOver: monitor.isOver()
    })
  })

  const handleClick = () => {
    onClick(position)
  }

  const squareClasses = [
    'square',
    isLight ? 'light' : 'dark',
    isSelected && 'selected',
    isValidMove && 'valid-move',
    isLastMove && 'last-move',
    isOver && 'drag-over'
  ].filter(Boolean).join(' ')

  return (
    <div
      ref={drop}
      className={squareClasses}
      onClick={handleClick}
      data-position={position}
    >
      {showRankLabel && (
        <span className="rank-label">{rank}</span>
      )}

      {showFileLabel && (
        <span className="file-label">{file}</span>
      )}

      {piece && (
        <Piece
          piece={piece}
          position={position}
        />
      )}

      {isValidMove && !piece && (
        <div className="move-indicator" />
      )}
    </div>
  )
}

export default Square

Piece Component

// components/Piece.jsx
import React from 'react'
import { useDrag } from 'react-dnd'

const PIECE_SYMBOLS = {
  white: {
    king: '♔', queen: '♕', rook: '♖',
    bishop: '♗', knight: '♘', pawn: '♙'
  },
  black: {
    king: '♚', queen: '♛', rook: '♜',
    bishop: '♝', knight: '♞', pawn: '♟'
  }
}

const Piece = ({ piece, position }) => {
  const [{ isDragging }, drag] = useDrag({
    type: 'piece',
    item: { position, piece },
    canDrag: () => canDragPiece(piece),
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    })
  })

  const pieceColor = getPieceColor(piece)
  const pieceType = getPieceType(piece)
  const symbol = PIECE_SYMBOLS[pieceColor]?.[pieceType] || ''

  const pieceClasses = [
    'piece',
    isDragging && 'dragging'
  ].filter(Boolean).join(' ')

  return (
    <div
      ref={drag}
      className={pieceClasses}
      style={{ opacity: isDragging ? 0.5 : 1 }}
    >
      {symbol}
    </div>
  )
}

const canDragPiece = (piece) => {
  if (!piece) return false
  // Only allow dragging white pieces (player pieces)
  return getPieceColor(piece) === 'white'
}

const getPieceColor = (piece) => {
  if (typeof piece === 'string') {
    return piece === piece.toUpperCase() ? 'white' : 'black'
  }
  return piece.color || 'white'
}

const getPieceType = (piece) => {
  const types = {
    'k': 'king', 'q': 'queen', 'r': 'rook',
    'b': 'bishop', 'n': 'knight', 'p': 'pawn'
  }

  if (typeof piece === 'string') {
    return types[piece.toLowerCase()] || 'pawn'
  }

  return piece.type || 'pawn'
}

export default Piece

Custom Hooks

useChessGame Hook

// hooks/useChessGame.js
import { useState, useCallback, useEffect } from 'react'
import { chessApi } from '../services/chessApi'

export const useChessGame = () => {
  const [gameState, setGameState] = useState({
    id: null,
    board: '',
    status: 'ready',
    activeColor: 'white',
    inCheck: false,
    gameOver: false,
    winner: null,
    lastMove: null
  })

  const [selectedSquare, setSelectedSquare] = useState(null)
  const [validMoves, setValidMoves] = useState([])
  const [moveHistory, setMoveHistory] = useState([])
  const [isPlayerTurn, setIsPlayerTurn] = useState(true)
  const [isLoading, setIsLoading] = useState(false)

  const createNewGame = useCallback(async () => {
    setIsLoading(true)

    try {
      const response = await chessApi.createGame({
        ai_enabled: true,
        difficulty: 'medium'
      })

      setGameState(response)
      setSelectedSquare(null)
      setValidMoves([])
      setMoveHistory([])
      setIsPlayerTurn(true)

      return response
    } catch (error) {
      console.error('Failed to create game:', error)
      throw error
    } finally {
      setIsLoading(false)
    }
  }, [])

  const makeMove = useCallback(async (from, to) => {
    if (!isPlayerTurn || !gameState.id) return

    setIsPlayerTurn(false)

    try {
      const response = await chessApi.makeMove(gameState.id, from, to)

      if (response.success) {
        setGameState(response.gameState)
        setMoveHistory(prev => [...prev, response.move])
        setSelectedSquare(null)
        setValidMoves([])

        if (!response.gameState.gameOver) {
          // Get AI move
          const aiResponse = await chessApi.getAIMove(gameState.id)

          if (aiResponse.success) {
            setGameState(aiResponse.gameState)
            setMoveHistory(prev => [...prev, aiResponse.move])
            setIsPlayerTurn(true)
          }
        }
      }

      return response
    } catch (error) {
      setIsPlayerTurn(true)
      throw error
    }
  }, [isPlayerTurn, gameState.id])

  const selectSquare = useCallback((position) => {
    setSelectedSquare(position)

    // Get valid moves for this position
    const moves = getValidMovesForPosition(position, gameState)
    setValidMoves(moves)
  }, [gameState])

  const clearSelection = useCallback(() => {
    setSelectedSquare(null)
    setValidMoves([])
  }, [])

  const getValidMovesForPosition = (position, state) => {
    // This should come from the API
    // Placeholder implementation
    return []
  }

  return {
    gameState,
    selectedSquare,
    validMoves,
    moveHistory,
    isPlayerTurn,
    isLoading,
    createNewGame,
    makeMove,
    selectSquare,
    clearSelection
  }
}

useWebSocket Hook

// hooks/useWebSocket.js
import { useEffect, useRef, useCallback } from 'react'

export const useWebSocket = (gameId, onMessage) => {
  const ws = useRef(null)
  const reconnectAttempts = useRef(0)
  const maxReconnectAttempts = 5

  const connect = useCallback(() => {
    if (!gameId) return

    try {
      const wsUrl = `ws://localhost:8080/ws/games/${gameId}`
      ws.current = new WebSocket(wsUrl)

      ws.current.onopen = () => {
        console.log('WebSocket connected')
        reconnectAttempts.current = 0
      }

      ws.current.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data)
          onMessage(data)
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error)
        }
      }

      ws.current.onclose = () => {
        console.log('WebSocket disconnected')

        // Attempt reconnection
        if (reconnectAttempts.current < maxReconnectAttempts) {
          setTimeout(() => {
            reconnectAttempts.current++
            connect()
          }, 1000 * Math.pow(2, reconnectAttempts.current))
        }
      }

      ws.current.onerror = (error) => {
        console.error('WebSocket error:', error)
      }
    } catch (error) {
      console.error('Failed to create WebSocket connection:', error)
    }
  }, [gameId, onMessage])

  const disconnect = useCallback(() => {
    if (ws.current) {
      ws.current.close()
      ws.current = null
    }
  }, [])

  useEffect(() => {
    connect()

    return () => {
      disconnect()
    }
  }, [connect, disconnect])

  return {
    connect,
    disconnect
  }
}

Error Boundary

// components/ErrorBoundary.jsx
import React from 'react'

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null, errorInfo: null }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      error,
      errorInfo
    })

    // Log error to monitoring service
    console.error('React Error Boundary caught an error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
          <button
            onClick={() => window.location.reload()}
            className="btn btn-primary"
          >
            Reload Page
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary

Testing

Component Testing with React Testing Library

// __tests__/ChessBoard.test.jsx
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import ChessBoard from '../components/ChessBoard'

const renderWithDnd = (component) => {
  return render(
    <DndProvider backend={HTML5Backend}>
      {component}
    </DndProvider>
  )
}

describe('ChessBoard', () => {
  const mockProps = {
    board: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR',
    selectedSquare: null,
    validMoves: [],
    lastMove: null,
    onSquareClick: jest.fn(),
    onPieceDrag: jest.fn()
  }

  test('renders chess board with 64 squares', () => {
    renderWithDnd(<ChessBoard {...mockProps} />)

    const squares = screen.getAllByTestId(/square-/)
    expect(squares).toHaveLength(64)
  })

  test('calls onSquareClick when square is clicked', () => {
    renderWithDnd(<ChessBoard {...mockProps} />)

    const square = screen.getByTestId('square-e4')
    fireEvent.click(square)

    expect(mockProps.onSquareClick).toHaveBeenCalledWith('e4')
  })

  test('highlights selected square', () => {
    const props = { ...mockProps, selectedSquare: 'e4' }
    renderWithDnd(<ChessBoard {...props} />)

    const square = screen.getByTestId('square-e4')
    expect(square).toHaveClass('selected')
  })
})

Hook Testing

// __tests__/useChessGame.test.js
import { renderHook, act } from '@testing-library/react'
import { useChessGame } from '../hooks/useChessGame'
import { chessApi } from '../services/chessApi'

jest.mock('../services/chessApi')

describe('useChessGame', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  test('creates new game', async () => {
    const mockGame = {
      id: 1,
      board: 'starting-position',
      status: 'in_progress'
    }

    chessApi.createGame.mockResolvedValue(mockGame)

    const { result } = renderHook(() => useChessGame())

    await act(async () => {
      await result.current.createNewGame()
    })

    expect(result.current.gameState).toEqual(mockGame)
    expect(chessApi.createGame).toHaveBeenCalledWith({
      ai_enabled: true,
      difficulty: 'medium'
    })
  })
})

Performance Optimization

Memoization

import React, { memo, useMemo, useCallback } from 'react'

const ChessSquare = memo(({ position, piece, isSelected, onClick }) => {
  const handleClick = useCallback(() => {
    onClick(position)
  }, [position, onClick])

  const squareClasses = useMemo(() => {
    return [
      'square',
      isSelected && 'selected'
    ].filter(Boolean).join(' ')
  }, [isSelected])

  return (
    <div className={squareClasses} onClick={handleClick}>
      {piece && <Piece piece={piece} />}
    </div>
  )
})

export default ChessSquare

Code Splitting

import React, { Suspense, lazy } from 'react'

const SettingsModal = lazy(() => import('./SettingsModal'))
const MoveHistory = lazy(() => import('./MoveHistory'))

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <SettingsModal />
        <MoveHistory />
      </Suspense>
    </div>
  )
}

Build Configuration

Vite Configuration

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3004,
    host: true
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          dnd: ['react-dnd', 'react-dnd-html5-backend']
        }
      }
    }
  },
  resolve: {
    alias: {
      '@': '/src'
    }
  }
})

Best Practices

  1. Use TypeScript for better type safety
  2. Implement proper error boundaries
  3. Memoize expensive computations with useMemo
  4. Use useCallback for event handlers
  5. Split components into smaller, focused pieces
  6. Test components thoroughly with React Testing Library
  7. Use context sparingly to avoid unnecessary re-renders

Next Steps

  1. Review the API Integration Guide for backend communication
  2. Check the Chess Features Guide for advanced chess logic
  3. See the UI/UX Guide for design improvements
  4. Compare with other framework implementations

The React implementation demonstrates how modern component-based architectures can create maintainable, testable chess applications with excellent developer experience and performance.

⚠️ **GitHub.com Fallback** ⚠️