Open17
React & Next.js勉強ログ

React公式

Next.js公式

pnpm
がインストールされてないからインストールしておく
$ brew install pnpm

npm install
→pnpm install
npx
→pnpm dlx
を使うらしい

まずはReact公式から
聞いてはいたけど、本当にフレームワークを使って始めようって書いてる

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

とりあえずプロジェクト作成
❯ 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

ディレクトリ構造はこんな感じ(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

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

型定義
typesというフォルダをプロジェクトのルートに作成し、その中にindex.d.tsを作成
tic-tac-toe/
├── types/
│ └── index.d.ts
types/index.d.ts
には以下を記載
type SquareValue = 'X' | 'O' | null;

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
を受け取る

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;
}

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>
);
}

スタイルの追加
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;
}

スタイルの適用
- 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>
);
}

Gameコンポーネントを表示
app/page.tsx
import Game from './components/Game';
export default function Home() {
return (
<main>
<Game />
</main>
);
}

アプリの起動
pnpm run dev