DEV Community

Sreeharsha
Sreeharsha

Posted on

Basic React Design Patterns: A practical look with Tic Tac Toe

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.
Image description
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
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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} />
      )}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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)