ゆるゆる Next.js

これは Next.js をゆるくまとめた記事です

大量の関数 & React コンポーネントが TypeScript の最新の記法とともにでてきます
初学者向けかと言われるとビミョー
まぁ使用例みたいなつもりです

その基礎文法辺りの解説はしないので各自で調べてください

今回はダイスを振るウェブページ「DiceS」を作ってみます
(なんとも言えないネーミングセンスでさーせん)

# 下ごしらえ

作業するフォルダに `cd` したら早速 `pnpm` で使う材料を用意します

```shell
mkdir dice-s
cd dice-s
pnpm add react react-dom next
```

`src/` の中にソースを書いていきます

お好きなエディタを研いでおいてください

私は Visual Studio Code 包丁を入れます

# はろわるど

```tsx:src/pages/index.js
export default () => <h1>Hello, World!</h1>;
```

これを書いて `pnpm dev` でデバッグ実行開始

Hello, World!

が見えたらこのままホットリローディングしながら作りましょ

# モデル

まず, このアプリの要件はこんな感じです
詳細に定義してはいけないので頭を悪くして書きます(　ﾟдﾟ)

- ダイスを振るボタンがありゅ！
- 振るダイスを変更するとこがありゅ！
- 振ったダイスを表示しゅりゅ！
- 振ったダイスの履歴が残りゅ！

まずはこの「ダイス」というモデルをプログラムで表現していこう

## ダイス

ここで以下を実装するね

- ダイスには個数と面の数がある
- 振ることで乱数生成器から乱数列を作る

```ts:src/model/dice.ts
export interface Dice {
  readonly quantity: number;
  readonly faces: number;
}

export interface NormalizedRng {
  (): number; // [0, 1) を返すことを想定
}

export const roll = (
  { quantity, faces }: Dice,
  rng: NormalizedRng,
): number[] =>
  [...new Array(quantity)].map(() => Math.floor(rng() * faces + 1));
```

うん, シンプルだね

# コントローラー

それじゃモデルと画面を橋渡しするこいつを考えていきましょー

1. アクション
2. リデューサー

## アクション

まず, アクション型という「画面からロジックへ伝えるメッセージの型」を作る

```ts:src/controller/action.ts
import type { Dice } from "model/dice";

export type Action = Readonly<{
  type: "ROLL_DICE"; // ダイスを えーい
} | {
  type: "CHANGE_DICE"; // このダイスが悪いんDA☆
  dice: Dice;
}>;
```

ダイスを振る `ROLL_DICE` とダイスを変える `CHANGE_DICE` の 2 つだね

## リデューサー

メデューサ？(難聴

> (更新前の状態, アクション) => 更新後の状態

という *更新前の状態* と *アクション* から **新しい状態を作る** 関数だよ

ここでは絶対に外部通信, 状態の持ち越し, 入出力などのいわゆる「副作用」を出しちゃいけないよ

```ts:controller/reducer.ts
import { Dice, roll, NormalizedRng } from "model/dice";
import type { Action } from "./action";

export interface State {
  readonly dice: Readonly<Dice>;
  readonly history: readonly number[][];
}

export const reducer = (rng: NormalizedRng) =>
  (state: State, action: Action): State => {
    switch (action.type) {
      case "CHANGE_DICE":
        return {
          ...state,
          dice: action.dice,
        };
      case "ROLL_DICE":
        const result = roll(state.dice, rng);
        console.log(result);
        return {
          ...state,
          history: [...state.history, result],
        };
    }
  };
```

## ビュー

そして表示する React コンポーネントたちだよ

私はデザイナーじゃないからびっくりするほどシンプル

```tsx:pages/index.tsx
import { FC, useReducer } from "react";
import type { NextPage } from "next";

import type { Dice } from "model/dice";
import { reducer } from "controller/reducer";

const RolledHistorty: FC<{ history: readonly number[][] }> = ({ history }) => (
  <>
    <div>
      {history
        .slice()
        .reverse()
        .map((results, i) => (
          <p key={i}>{results.join(" , ")}</p>
        ))}
    </div>
    <style jsx>{`
      div {
        position: fixed;
        width: 80%;
        height: 75%;
        overflow: auto;
        bottom: 2rem;
      }
    `}</style>
  </>
);

const PositiveNumberInput: FC<{
  readonly defaultValue: number;
  readonly onChange: (newValue: number) => void;
}> = ({ defaultValue, onChange }) => (
  <input
    type="number"
    onChange={(e) => {
      const num = parseInt(e.target.value, 10);
      if (!(0 < num)) return;
      onChange(num);
    }}
    min={1}
    defaultValue={defaultValue}
  />
);

const DiceInput: FC<{
  readonly changeDice: (dice: Dice) => void;
  readonly rollDice: () => void;
  readonly dice: Dice;
}> = ({ changeDice, rollDice, dice: { quantity, faces } }) => {
  return (
    <>
      <label>nDm でダイスの種類を入力</label>
      <PositiveNumberInput
        onChange={(quantity) => changeDice({ quantity, faces })}
        defaultValue={quantity}
      />
      D
      <PositiveNumberInput
        onChange={(faces) => changeDice({ quantity, faces })}
        defaultValue={faces}
      />
      <button onClick={rollDice}>Roll</button>
    </>
  );
};

const Index: NextPage = () => {
  const [state, dispatch] = useReducer(reducer(Math.random), {
    dice: { quantity: 2, faces: 6 } as Dice,
    history: [],
  });
  const { dice, history } = state;
  return (
    <>
      <DiceInput
        changeDice={(dice) => dispatch({ type: "CHANGE_DICE", dice })}
        rollDice={() => dispatch({ type: "ROLL_DICE" })}
        dice={dice}
      />
      <RolledHistorty history={history} />
    </>
  );
};

export default Index;
```

# まとめ

いままで作った流れをまとめておこう

1. 最低限やることを決める - まず脳死で要件を決めた (重要)
2. モデルを作る - ダイスって概念をプログラムに書き起こした
3. コントローラーうぉ作る - モデルと画面を分離するためにアクションとリデューサーを明確にした
4. ウェブを描く - やりたい画面を実現するためにコンポーネントをどんどん作った (ファイルを分けようかと思ったけど記事的に見辛くなりそうなのでやめた)

# おわりに

ここまで読んでくれた方向けにソースコードがまるっと載った [レポジトリ](https://github.com/MikuroXina/DiceS) を貼っておきます

[Vercel](https://vercel.com/home) とかでテキトーにデプロイして遊んでくだされ