これは Next.js をゆるくまとめた記事です
大量の関数 & React コンポーネントが TypeScript の最新の記法とともにでてきます
初学者向けかと言われるとビミョー
まぁ使用例みたいなつもりです
その基礎文法辺りの解説はしないので各自で調べてください
今回はダイスを振るウェブページ「DiceS」を作ってみます
(なんとも言えないネーミングセンスでさーせん)
下ごしらえ
作業するフォルダに cd
したら早速 pnpm
で使う材料を用意します
mkdir dice-s
cd dice-s
pnpm add react react-dom next
src/
の中にソースを書いていきます
お好きなエディタを研いでおいてください
私は Visual Studio Code 包丁を入れます
はろわるど
export default () => <h1>Hello, World!</h1>;
これを書いて pnpm dev
でデバッグ実行開始
Hello, World!
が見えたらこのままホットリローディングしながら作りましょ
モデル
まず, このアプリの要件はこんな感じです
詳細に定義してはいけないので頭を悪くして書きます( ゚д゚)
- ダイスを振るボタンがありゅ!
- 振るダイスを変更するとこがありゅ!
- 振ったダイスを表示しゅりゅ!
- 振ったダイスの履歴が残りゅ!
まずはこの「ダイス」というモデルをプログラムで表現していこう
ダイス
ここで以下を実装するね
- ダイスには個数と面の数がある
- 振ることで乱数生成器から乱数列を作る
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));
うん, シンプルだね
コントローラー
それじゃモデルと画面を橋渡しするこいつを考えていきましょー
- アクション
- リデューサー
アクション
まず, アクション型という「画面からロジックへ伝えるメッセージの型」を作る
import type { Dice } from "model/dice";
export type Action = Readonly<{
type: "ROLL_DICE"; // ダイスを えーい
} | {
type: "CHANGE_DICE"; // このダイスが悪いんDA☆
dice: Dice;
}>;
ダイスを振る ROLL_DICE
とダイスを変える CHANGE_DICE
の 2 つだね
リデューサー
メデューサ?(難聴
(更新前の状態, アクション) => 更新後の状態
という 更新前の状態 と アクション から 新しい状態を作る 関数だよ
ここでは絶対に外部通信, 状態の持ち越し, 入出力などのいわゆる「副作用」を出しちゃいけないよ
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 コンポーネントたちだよ
私はデザイナーじゃないからびっくりするほどシンプル
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;
まとめ
いままで作った流れをまとめておこう
- 最低限やることを決める - まず脳死で要件を決めた (重要)
- モデルを作る - ダイスって概念をプログラムに書き起こした
- コントローラーうぉ作る - モデルと画面を分離するためにアクションとリデューサーを明確にした
- ウェブを描く - やりたい画面を実現するためにコンポーネントをどんどん作った (ファイルを分けようかと思ったけど記事的に見辛くなりそうなのでやめた)
おわりに
ここまで読んでくれた方向けにソースコードがまるっと載った レポジトリ を貼っておきます
Vercel とかでテキトーにデプロイして遊んでくだされ