Introduction to React Numbers Games
Numbers games are fantastic for cognitive development and entertainment. In this comprehensive React tutorial, you'll learn how to build an interactive numbers game from scratch using modern React with Hooks. This project is perfect for React beginners looking to practice state management, component composition, and local storage integration.
Learn how to create an engaging numbers game using React Hooks with scoring, graphics, and local storage persistence.
Why Build a Numbers Game with React?
Educational Value: Great for learning React fundamentals
Cognitive Benefits: Number games improve mental math skills
Portfolio Project: Demonstrates key React concepts in action
Customizable: Easy to extend with new features
Prerequisites
Before we begin, ensure you have:
Basic knowledge of JavaScript and React
Node.js installed on your system
A code editor (VS Code recommended)
Project Setup
Let's start by creating our React project:
npx create-react-app numbers-game cd numbers-game npm start
Building the Numbers Game Components
1. Main App Component with Tab Navigation
// App.js import React, { useState, useEffect } from 'react'; import './App.css'; import GameBoard from './components/GameBoard'; import ScoreHistory from './components/ScoreHistory'; import Instructions from './components/Instructions'; function App() { const [activeTab, setActiveTab] = useState('game'); const [scores, setScores] = useState([]); // Load scores from localStorage on component mount useEffect(() => { const savedScores = localStorage.getItem('numberGameScores'); if (savedScores) { setScores(JSON.parse(savedScores)); } }, []); // Save scores to localStorage whenever scores change useEffect(() => { localStorage.setItem('numberGameScores', JSON.stringify(scores)); }, [scores]); const addScore = (newScore) => { const scoreWithDate = { ...newScore, date: new Date().toLocaleString(), id: Date.now() }; setScores(prevScores => [scoreWithDate, ...prevScores].slice(0, 20)); // Keep last 20 scores }; const tabs = [ { id: 'game', label: 'Play Game' }, { id: 'history', label: 'Score History' }, { id: 'instructions', label: 'How to Play' } ]; return ( <div className="app"> <header className="app-header"> <h1>React Numbers Challenge</h1> <p>Test your math skills with this engaging numbers game!</p> </header> <nav className="tab-navigation"> {tabs.map(tab => ( <button key={tab.id} className={`tab-button ${activeTab === tab.id ? 'active' : ''}`} onClick={() => setActiveTab(tab.id)} > {tab.label} </button> ))} </nav> <main className="tab-content"> {activeTab === 'game' && <GameBoard onScoreSubmit={addScore} />} {activeTab === 'history' && <ScoreHistory scores={scores} />} {activeTab === 'instructions' && <Instructions />} </main> </div> ); } export default App;
2. Game Board Component
// components/GameBoard.js import React, { useState, useEffect } from 'react'; const GameBoard = ({ onScoreSubmit }) => { const [numbers, setNumbers] = useState([]); const [target, setTarget] = useState(0); const [selectedNumbers, setSelectedNumbers] = useState([]); const [currentSum, setCurrentSum] = useState(0); const [score, setScore] = useState(0); const [timeLeft, setTimeLeft] = useState(60); const [gameStatus, setGameStatus] = useState('idle'); // idle, playing, finished const [message, setMessage] = useState(''); const generateNumbers = () => { const newNumbers = []; for (let i = 0; i < 6; i++) { newNumbers.push(Math.floor(Math.random() * 10) + 1); } setNumbers(newNumbers); setTarget(Math.floor(Math.random() * 20) + 10); }; const startGame = () => { generateNumbers(); setSelectedNumbers([]); setCurrentSum(0); setScore(0); setTimeLeft(60); setGameStatus('playing'); setMessage('Find number combinations that sum to the target!'); }; const handleNumberClick = (number, index) => { if (gameStatus !== 'playing') return; const newSelected = [...selectedNumbers, { number, index }]; setSelectedNumbers(newSelected); const newSum = currentSum + number; setCurrentSum(newSum); if (newSum === target) { const points = selectedNumbers.length === 0 ? 10 : 5; // Bonus for single number matches const newScore = score + points; setScore(newScore); setMessage(`Perfect! +${points} points!`); setTimeout(() => { generateNumbers(); setSelectedNumbers([]); setCurrentSum(0); setMessage('Great! Try another combination.'); }, 1000); } else if (newSum > target) { setMessage('Too high! Try different numbers.'); setTimeout(() => { setSelectedNumbers([]); setCurrentSum(0); setMessage('Select numbers that sum to the target.'); }, 1000); } }; const resetSelection = () => { setSelectedNumbers([]); setCurrentSum(0); setMessage('Selection reset. Try again!'); }; const endGame = () => { setGameStatus('finished'); onScoreSubmit({ score, time: 60 - timeLeft }); setMessage(`Game Over! Final Score: ${score}`); }; useEffect(() => { let timer; if (gameStatus === 'playing' && timeLeft > 0) { timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000); } else if (gameStatus === 'playing' && timeLeft === 0) { endGame(); } return () => clearTimeout(timer); }, [gameStatus, timeLeft]); const getNumberColor = (number) => { const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F' ]; return colors[number % colors.length]; }; return ( <div className="game-board"> <div className="game-info"> <div className="target-display"> <h2>Target: {target}</h2> </div> <div className="score-time"> <div className="score">Score: {score}</div> <div className="timer">Time: {timeLeft}s</div> </div> </div> {gameStatus === 'idle' && ( <div className="game-start"> <button className="start-button" onClick={startGame}> Start Game </button> </div> )} {gameStatus !== 'idle' && ( <> <div className="numbers-grid"> {numbers.map((number, index) => { const isSelected = selectedNumbers.some(item => item.index === index); return ( <button key={index} className={`number-card ${isSelected ? 'selected' : ''}`} style={{ backgroundColor: getNumberColor(number) }} onClick={() => handleNumberClick(number, index)} disabled={isSelected || gameStatus !== 'playing'} > {number} </button> ); })} </div> <div className="current-selection"> <h3>Current Selection: {selectedNumbers.map(item => item.number).join(' + ')}</h3> <h3>Current Sum: {currentSum}</h3> {selectedNumbers.length > 0 && ( <button className="reset-button" onClick={resetSelection}> Reset Selection </button> )} </div> {gameStatus === 'playing' && ( <button className="end-game-button" onClick={endGame}> End Game Early </button> )} </> )} {message && <div className="game-message">{message}</div>} {gameStatus === 'finished' && ( <button className="restart-button" onClick={startGame}> Play Again </button> )} </div> ); }; export default GameBoard;
3. Score History Component
// components/ScoreHistory.js import React from 'react'; const ScoreHistory = ({ scores }) => { const getScoreColor = (score) => { if (score >= 50) return '#4CAF50'; if (score >= 30) return '#FFC107'; if (score >= 10) return '#FF9800'; return '#F44336'; }; return ( <div className="score-history"> <h2>Score History</h2> {scores.length === 0 ? ( <p className="no-scores">No scores yet. Play the game to see your history!</p> ) : ( <div className="scores-list"> {scores.map((score, index) => ( <div key={score.id} className="score-item"> <div className="score-rank">#{index + 1}</div> <div className="score-value" style={{ color: getScoreColor(score.score) }} > {score.score} points </div> <div className="score-time">{score.time} seconds</div> <div className="score-date">{score.date}</div> </div> ))} </div> )} </div> ); }; export default ScoreHistory;
4. Instructions Component
// components/Instructions.js import React from 'react'; const Instructions = () => { return ( <div className="instructions"> <h2>How to Play React Numbers Challenge</h2> <div className="instruction-section"> <h3>🎯 Game Objective</h3> <p>Select numbers that add up to the target number displayed at the top of the screen.</p> </div> <div className="instruction-section"> <h3>⭐ Scoring System</h3> <ul> <li><strong>Single Number Match:</strong> 10 points (when one number equals the target)</li> <li><strong>Multiple Number Combination:</strong> 5 points (when multiple numbers sum to the target)</li> <li><strong>Time Bonus:</strong> Higher scores for faster completions!</li> </ul> </div> <div className="instruction-section"> <h3>⏰ Game Rules</h3> <ul> <li>Each game lasts 60 seconds</li> <li>You can select multiple numbers to form sums</li> <li>If your sum exceeds the target, your selection resets</li> <li>When you match the target exactly, you get points and new numbers appear</li> </ul> </div> <div className="instruction-section"> <h3>🎮 Controls</h3> <ul> <li><strong>Click numbers</strong> to select them</li> <li><strong>Reset Selection</strong> to clear your current choices</li> <li><strong>End Game Early</strong> if you want to finish before time runs out</li> </ul> </div> <div className="pro-tips"> <h3>💡 Pro Tips</h3> <ul> <li>Look for single numbers that match the target first - they give more points!</li> <li>Plan your combinations before clicking</li> <li>Practice mental math to improve your speed</li> <li>Check your score history to track improvement</li> </ul> </div> </div> ); }; export default Instructions;
Styling the Game
Create an App.css file with modern, responsive styling:
/* App.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; color: #333; } .app { max-width: 800px; margin: 0 auto; padding: 20px; min-height: 100vh; } .app-header { text-align: center; margin-bottom: 30px; color: white; } .app-header h1 { font-size: 2.5rem; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } .app-header p { font-size: 1.1rem; opacity: 0.9; } /* Tab Navigation */ .tab-navigation { display: flex; background: white; border-radius: 10px; padding: 5px; margin-bottom: 30px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .tab-button { flex: 1; padding: 15px 20px; border: none; background: transparent; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; color: #666; } .tab-button.active { background: linear-gradient(135deg, #667eea, #764ba2); color: white; box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4); } .tab-button:hover:not(.active) { background: #f8f9fa; color: #333; } /* Tab Content */ .tab-content { background: white; border-radius: 15px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); min-height: 400px; } /* Game Board */ .game-board { text-align: center; } .game-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding: 20px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 10px; color: white; } .target-display h2 { font-size: 2rem; margin: 0; } .score-time { display: flex; gap: 20px; font-size: 1.2rem; font-weight: bold; } /* Numbers Grid */ .numbers-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 30px; } .number-card { aspect-ratio: 1; border: none; border-radius: 15px; font-size: 2rem; font-weight: bold; color: white; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(0,0,0,0.2); } .number-card:hover:not(:disabled) { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); } .number-card.selected { transform: scale(0.9); opacity: 0.7; cursor: not-allowed; } .number-card:disabled { opacity: 0.5; cursor: not-allowed; } /* Buttons */ .start-button, .reset-button, .end-game-button, .restart-button { padding: 12px 30px; border: none; border-radius: 25px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin: 10px; } .start-button, .restart-button { background: linear-gradient(135deg, #4CAF50, #45a049); color: white; } .start-button:hover, .restart-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); } .reset-button { background: linear-gradient(135deg, #FF9800, #F57C00); color: white; } .reset-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4); } .end-game-button { background: linear-gradient(135deg, #f44336, #d32f2f); color: white; } .end-game-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4); } /* Current Selection */ .current-selection { background: #f8f9fa; padding: 20px; border-radius: 10px; margin: 20px 0; } .current-selection h3 { margin: 10px 0; color: #555; } /* Game Message */ .game-message { padding: 15px; margin: 20px 0; border-radius: 10px; font-weight: 600; background: #e3f2fd; color: #1976d2; border-left: 4px solid #2196f3; } /* Score History */ .score-history h2 { text-align: center; margin-bottom: 30px; color: #333; } .no-scores { text-align: center; color: #666; font-style: italic; padding: 40px; } .scores-list { display: flex; flex-direction: column; gap: 15px; } .score-item { display: flex; align-items: center; padding: 15px 20px; background: #f8f9fa; border-radius: 10px; transition: transform 0.2s ease; } .score-item:hover { transform: translateX(5px); background: #e9ecef; } .score-rank { font-weight: bold; color: #666; min-width: 50px; } .score-value { font-size: 1.2rem; font-weight: bold; flex: 1; } .score-time, .score-date { color: #666; min-width: 120px; text-align: right; } /* Instructions */ .instructions h2 { text-align: center; margin-bottom: 30px; color: #333; } .instruction-section { margin-bottom: 30px; padding: 20px; background: #f8f9fa; border-radius: 10px; border-left: 4px solid #667eea; } .instruction-section h3 { margin-bottom: 15px; color: #333; } .instruction-section ul { list-style-type: none; padding-left: 0; } .instruction-section li { padding: 8px 0; padding-left: 25px; position: relative; } .instruction-section li:before { content: '✓'; position: absolute; left: 0; color: #4CAF50; font-weight: bold; } .pro-tips { background: linear-gradient(135deg, #fff3cd, #ffeaa7); border-left: 4px solid #ffc107; } /* Responsive Design */ @media (max-width: 768px) { .app { padding: 10px; } .numbers-grid { grid-template-columns: repeat(2, 1fr); } .game-info { flex-direction: column; gap: 15px; text-align: center; } .score-item { flex-direction: column; text-align: center; gap: 10px; } .score-time, .score-date { text-align: center; } .tab-button { padding: 12px 10px; font-size: 0.9rem; } }
Running the Project
Installation and Setup
Create the React App:
npx create-react-app numbers-game cd numbers-gameReplace Default Files:
Replace the contents of
src/App.jswith our main component codeReplace
src/App.csswith our CSS stylesCreate a
src/componentsdirectory and add the three component files
Project Structure:
numbers-game/ ├── public/ ├── src/ │ ├── components/ │ │ ├── GameBoard.js │ │ ├── ScoreHistory.js │ │ └── Instructions.js │ ├── App.js │ ├── App.css │ └── index.js └── package.json
Start Development Server:
npm startBuild for Production:
npm run build
SEO Optimization Tips
To maximize your React numbers game's visibility in search results:
Meta Tags: Add relevant meta descriptions in
public/index.htmlStructured Data: Implement JSON-LD for educational games
Performance: Use React.lazy() for code splitting
Accessibility: Ensure proper ARIA labels and keyboard navigation
Mobile Optimization: Implement responsive design as shown above
Conclusion
You've successfully built a feature-rich numbers game using React Hooks! This project demonstrates:
State Management with useState and useEffect
Component Composition and reusability
Local Storage Integration for data persistence
Responsive Design with modern CSS
Tab-based Navigation for better UX
Scoring System with history tracking
Next Steps to Enhance Your Game:
Add difficulty levels
Implement sound effects
Create multiplayer functionality
Add achievement system
Integrate with backend for global leaderboards
This numbers game is perfect for your React portfolio and demonstrates practical application of key React concepts. Happy coding!

Comments