Session 4: Tic-Tac-Toe with React, No npm Required
Goal: Learn React by building a Tic-Tac-Toe game in a single HTML file using CDN scripts instead of npm
Prerequisites: Sessions 1-3 (HTML, CSS, JavaScript, GitHub Pages)
Why This Version of React?
Normally React projects use tools like Vite and npm, but that only works when students can install packages.
For this class, we can still teach the important React ideas without any installs:
- We load React from a CDN
- We write everything in one
react-tictactoe.htmlfile - Students can run it with Live Server or open the file directly
- GitHub Pages can publish it just like the earlier games
This is a good teaching setup even if it is not the full production workflow.
What You Will Build Today
A Tic-Tac-Toe game with:
- Reusable React components
- State that updates the screen automatically
- Win and draw detection
- A reset button
- A file that can be added directly to the existing portfolio
Part 1: Create the File
Create a new file named react-tictactoe.html.
Paste this starter structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Tic-Tac-Toe</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #312e81, #1d4ed8, #0891b2);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: white;
padding: 24px;
}
.game {
text-align: center;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 20px;
padding: 32px;
box-shadow: 0 24px 50px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
h1 {
font-size: 42px;
margin-bottom: 10px;
}
.status {
min-height: 32px;
font-size: 22px;
font-weight: bold;
margin: 18px 0;
}
.board {
display: grid;
grid-template-columns: repeat(3, 110px);
gap: 10px;
margin: 0 auto 20px;
width: fit-content;
}
.square {
width: 110px;
height: 110px;
border: none;
border-radius: 14px;
font-size: 42px;
font-weight: bold;
cursor: pointer;
background: white;
color: #1d4ed8;
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.2);
}
.reset-button {
border: none;
border-radius: 999px;
background: #22c55e;
color: white;
font-size: 18px;
font-weight: bold;
padding: 12px 22px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState } = React;
function App() {
return (
<div className="game">
<h1>Tic-Tac-Toe</h1>
<p>React can run in one HTML file too.</p>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
Open it with Live Server. If students see the heading, the setup is working.
Part 2: Understand the Three React Pieces
1. Components
Components are functions that return UI.
function Welcome() {
return <h2>Hello!</h2>;
}
2. JSX
JSX lets us write HTML-like code inside JavaScript.
const name = 'Player 1';
return <p>Hello, {name}</p>;
3. State
State is data that can change and update the page automatically.
const [count, setCount] = useState(0);
When setCount(...) runs, React redraws the page with the new value.
Part 3: Add the Game Logic
Replace the code inside <script type="text/babel"> with this full version:
<script type="text/babel">
const { useState } = React;
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ squares, xIsNext, onPlay }) {
function handleClick(index) {
if (squares[index] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
nextSquares[index] = xIsNext ? 'X' : 'O';
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else if (squares.every(square => square)) {
status = 'Draw!';
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
function App() {
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
function handlePlay(nextSquares) {
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
function resetGame() {
setSquares(Array(9).fill(null));
setXIsNext(true);
}
return (
<div className="game">
<h1>Tic-Tac-Toe</h1>
<p>Built with React, but no package install needed.</p>
<Board squares={squares} xIsNext={xIsNext} onPlay={handlePlay} />
<button className="reset-button" onClick={resetGame}>New Game</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
How This Connects to Earlier Sessions
Students already know:
- Variables
- Functions
- Arrays
- Conditionals
- Event-driven logic
React adds a cleaner structure on top of those ideas:
Squareis a reusable piece of UIBoardgroups multiple squares togetherAppstores the main game stateuseStatereplaces manual DOM updates in many cases
So this is not starting over. It is reorganizing the same JavaScript ideas in a more scalable way.
Part 4: Add It to the Portfolio
Go back to the portfolio index.html from Session 3 and change the “Coming Next” card.
Replace it with:
<section class="game-card">
<h2>React Tic-Tac-Toe</h2>
<p>A classic game rebuilt with React components and state.</p>
<div class="tech-tags">
<span>React</span>
<span>Components</span>
<span>JSX</span>
</div>
<a class="play-button" href="react-tictactoe.html">Play</a>
</section>
Then publish the update:
git add .
git commit -m "Add React tic-tac-toe"
git push origin main
Because the React version is still just an HTML file, GitHub Pages can host it directly.
Customization Ideas
- Use emoji instead of X and O
- Change the board colors
- Add a score tracker for multiple rounds
- Add player name inputs
- Highlight the winning line
What Students Learned Today
- React components: splitting UI into pieces
- JSX: writing HTML-like syntax inside JavaScript
- State: updating the UI by changing data
- Props: passing data to child components
- CDN workflow: using a library without package installation
- Portfolio growth: adding a new project to an existing published site
Common Issues
Problem: “The page is blank”
Solution: Check that the script tag uses type="text/babel" and that all CDN script tags loaded correctly.
Problem: “React is not defined”
Solution: Make sure the React CDN scripts are above the Babel script block.
Problem: “Nothing happens when I click a square”
Solution: Check that onSquareClick={() => handleClick(...)} is passed to each Square.
Problem: “The game never resets to X first”
Solution: In resetGame(), set both the board and xIsNext back to their starting values.
Resources
Session 3 | Back to GameCraft Home