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>
);
}
ELI5: Think of it like making a sandwich. The Board is the whole sandwich, and each Square is like a slice of cheese. We put many cheese slices together to make our sandwich, just like we use many Squares to make our Board.
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]);
ELI5: Hooks are like magic spells. useState is a spell that helps our game remember things (like whose turn it is). useEffect is a spell that watches for changes and does something when they happen (like making the computer take its turn).
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>
);
}
ELI5: Imagine you're putting on a puppet show. The Container Component is like the puppeteer who controls everything behind the scenes. The Presentational Component is like the puppet that the audience sees. The puppeteer (Container) does all the thinking and moving, while the puppet (Presentational) just shows what it's told to show.
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).
ELI5: Think of a stateful component like a teacher in a classroom. The teacher (TicTacToe) keeps track of everything that's happening. The students (Square, Board, Score) just do what they're told without keeping track of anything themselves.
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);
ELI5: Imagine you have a toy car. An HOC is like a special machine that can add cool features to your toy car, like flashing lights or a remote control. You put your car into the machine, and it comes out with new superpowers!
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} />
)}
/>
);
}
ELI5: This is like having a coloring book where you can choose different crayons. The coloring book (GameLogic) gives you the outline, but you get to decide what colors to use (how to render it) each time you use it.
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)