TLDR:
This article explores React design patterns through a Tic Tac Toe game implemented with TypeScript, covering:
- Functional Components with TypeScript
- Hooks for State Management
- Prop Typing for Type Safety
- Composition of Components
- Container and Presentational Components
- Stateful and Stateless Components
- Higher-Order Components (HOCs)
- Render Props
These patterns enhance:
- Type safety
- Code organization
- Reusability
- Maintainability
- Separation of concerns
- Testability of React applications
Introduction
React, combined with TypeScript, offers a powerful toolkit for building robust web applications. Let's explore both fundamental and advanced React patterns using a Tic Tac Toe game as our example, providing simple explanations for each pattern.

You can reference the code here while going through the article for more clarity: github
Project Structure
Our Tic Tac Toe project is organized like this:
tic-tac-toe
├── src/
│ ├── components/
│ │ ├── ui/ # Reusable UI components
│ │ ├── Board.tsx # Tic Tac Toe board
│ │ ├── Game.tsx # Game component
│ │ ├── Square.tsx # Square component
│ │ └── Score.tsx # Score component
│ ├── App.tsx # Main app component
│ └── main.tsx # Entry point
| ... # more config files
Design Patterns and Implementation:
1. Functional Components and Prop Typing:
Functional components with explicitly typed props ensure type safety and self-documenting code.
// Square.tsx
export default function Square(
{ value, onClick }: { value: string | null, onClick: () => void }
) {
return (
<button className={styles.square} onClick={onClick}>
{value}
</button>
);
}
ELI5: Imagine you're building with special Lego bricks. Each brick (component) has a specific shape (props) that only fits in certain places. TypeScript is like a magic ruler that makes sure you're using the right bricks in the right spots.
2. Composition:
Building complex UIs from smaller, reusable components promotes modularity and reusability.
// Board.tsx
export default function Board({board, handleClick}: { board: (null | string)[], handleClick: (index: number) => void }) {
return (
<div className={styles.board_items}>
{board.map((value, index) => (
<Square key={index} value={value} onClick={() => handleClick(index)} />
))}
</div>
);
}
3. State Management with Hooks:
Using useState and useEffect hooks simplifies state management in functional components.
// Game.tsx
const [board, setBoard] = useState<(null | string)[]>(Array(9).fill(null));
const [currentPlayer, setCurrentPlayer] = useState("X");
const [gameOver, setGameOver] = useState(false);
useEffect(() => {
if (aiOpponent && currentPlayer === "O" && !gameOver) {
const timer = setTimeout(makeAIMove, 500);
return () => clearTimeout(timer);
}
}, [board, currentPlayer]);
4. Container and Presentational Components:
This pattern separates logic (containers) from rendering (presentational components).
// Game.tsx (Container Component)
export default function TicTacToe({ aiOpponent = false }) {
const [board, setBoard] = useState<(null | string)[]>(Array(9).fill(null));
const [currentPlayer, setCurrentPlayer] = useState("X");
// ... game logic
return (
<div className={styles.wrapper}>
<Board board={board} handleClick={handleClick} />
<Score x_wins={xWins} o_wins={oWins} />
</div>
);
}
// Board.tsx (Presentational Component)
export default function Board({board, handleClick}) {
return (
<div className={styles.board_items}>
{board.map((value, index) => (
<Square key={index} value={value} onClick={() => handleClick(index)} />
))}
</div>
);
}
5. Stateful and Stateless Components:
This concept involves minimizing the number of stateful components to simplify data flow. In our implementation, TicTacToe
is stateful (manages game state), while Square
, Board
, and Score
are stateless (receive data via props).
6. Higher-Order Components (HOCs):
HOCs are functions that take a component and return a new component with additional props or behavior.
function withAIOpponent(WrappedComponent) {
return function(props) {
const [aiEnabled, setAiEnabled] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={aiEnabled}
onChange={() => setAiEnabled(!aiEnabled)}
/>
Play against AI
</label>
<WrappedComponent {...props} aiOpponent={aiEnabled} />
</>
);
}
}
const TicTacToeWithAI = withAIOpponent(TicTacToe);
7. Render Props:
This pattern involves passing rendering logic as a prop to a component.
function GameLogic({ render }) {
const [board, setBoard] = useState(Array(9).fill(null));
// ... game logic
return render({ board, handleClick });
}
function App() {
return (
<GameLogic
render={({ board, handleClick }) => (
<Board board={board} handleClick={handleClick} />
)}
/>
);
}
Conclusion:
By implementing these design patterns in our TypeScript-based Tic Tac Toe game, we've created a modular, type-safe, and maintainable application. These patterns promote:
- Clean and efficient code
- Improved readability
- Enhanced scalability
- Better separation of concerns
- Increased reusability
- Easier testing and debugging
As you build more complex React applications, these patterns will serve as valuable tools in your development toolkit, allowing you to create applications that are not only functional but also clean, efficient, and easy to maintain and extend.
Top comments (0)