Open17

React & Next.js勉強ログ

bbtitbbtit

チュートリアルの五目並べをしようと思うけど、App RouterのNext.jsの勝手が分からないからGPTに相談しながら進める

bbtitbbtit

とりあえずプロジェクト作成

❯ pnpm create next-app@latest
.../19471ce4ff4-4efb                     |   +1 +
.../19471ce4ff4-4efb                     | Progress: resolved 1, reused 0, downloaded 1, added 1, done
✔ What is your project named? … tic-tac-toe
✔ Would you like to use TypeScript? … No / Yes✔
✔ Would you like to use ESLint? … No / Yes✔
✔ Would you like to use Tailwind CSS? … No✔ / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes✔
✔ Would you like to use App Router? (recommended) … No / Yes✔
✔ Would you like to use Turbopack for `next dev`? … No / Yes✔
✔ Would you like to customize the import alias (`@/*` by default)? … No✔ / Yes

~ took 1m24s
❯ cd tic-tac-toe
bbtitbbtit

ディレクトリ構造はこんな感じ(node_modulesは除外)

tic-tac-toe on  main via  v23.1.0
❯ tree -I "node_modules"
.
├── README.md
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package.json
├── pnpm-lock.yaml
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── next.svg
│   ├── vercel.svg
│   └── window.svg
├── src
│   └── app
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       ├── page.module.css
│       └── page.tsx
└── tsconfig.json

4 directories, 17 files
bbtitbbtit

❯ pnpm run dev
http://localhost:3000にアクセスして画面が表示されることを確認

bbtitbbtit

型定義
typesというフォルダをプロジェクトのルートに作成し、その中にindex.d.tsを作成

tic-tac-toe/
├── types/
│   └── index.d.ts

types/index.d.tsには以下を記載

type SquareValue = 'X' | 'O' | null;
bbtitbbtit

Squareコンポーネントの作成

app/components/Square.tsx
'use client';

type SquareProps = {
  value: SquareValue;
  onClick: () => void;
};

export default function Square({ value, onClick }: SquareProps) {
  return (
    <button className="square" onClick={onClick}>
      {value}
    </button>
  );
}
  • Squareコンポーネントは、ボタン内に表示するvalueと、押下時に発火する関数であるonClickを受け取る
bbtitbbtit

Boardコンポーネントの作成

app/components/Board.tsx
'use client';

import Square from './Square';

type BoardProps = {
  squares: SquareValue[];
  xIsNext: boolean;
  onPlay: (nextSquares: SquareValue[]) => void;
};

export default function Board({ squares, xIsNext, onPlay }: BoardProps) {
  function handleClick(i: number) {
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    const nextSquares = squares.slice();
    nextSquares[i] = xIsNext ? 'X' : 'O';
    onPlay(nextSquares);
  }

  const winner = calculateWinner(squares);
  let status: string;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} onClick={() => handleClick(0)} />
        <Square value={squares[1]} onClick={() => handleClick(1)} />
        <Square value={squares[2]} onClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={squares[3]} onClick={() => handleClick(3)} />
        <Square value={squares[4]} onClick={() => handleClick(4)} />
        <Square value={squares[5]} onClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={squares[6]} onClick={() => handleClick(6)} />
        <Square value={squares[7]} onClick={() => handleClick(7)} />
        <Square value={squares[8]} onClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares: SquareValue[]): SquareValue {
  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 (const [a, b, c] of lines) {
    if (
      squares[a] &&
      squares[a] === squares[b] &&
      squares[a] === squares[c]
    ) {
      return squares[a];
    }
  }
  return null;
}
bbtitbbtit

Gameコンポーネントの作成

app/components/Game.tsx
'use client';

import { useState } from 'react';
import Board from './Board';

export default function Game() {
  const [history, setHistory] = useState<SquareValue[][]>([Array(9).fill(null)]);
  const [currentMove, setCurrentMove] = useState<number>(0);
  const xIsNext = currentMove % 2 === 0;
  const currentSquares = history[currentMove];

  function handlePlay(nextSquares: SquareValue[]) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove: number) {
    setCurrentMove(nextMove);
  }

  const moves = history.map((_, move) => {
    const description = move > 0 ? 'Go to move #' + move : 'Go to game start';
    return (
      <li key={move}>
        <button onClick={() => jumpTo(move)}>{description}</button>
      </li>
    );
  });

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}
bbtitbbtit

スタイルの追加

app/styles/globals.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}
bbtitbbtit

スタイルの適用

  • Next.jsでは、デフォルトでapp/globals.cssがインポートされている
  • app/layout.tsxで他のCSSをインポート
app/layout.tsx
import './styles/globals.css';

export const metadata = {
  title: 'Tic Tac Toe',
  description: 'Next.jsで作る五目並べゲーム',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}
bbtitbbtit

Gameコンポーネントを表示

app/page.tsx
import Game from './components/Game';

export default function Home() {
  return (
    <main>
      <Game />
    </main>
  );
}