LoginSignup
4
5

More than 3 years have passed since last update.

ゆるゆる Next.js

Last updated at Posted at 2019-04-07

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

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

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

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

下ごしらえ

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

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

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

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

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

はろわるど

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

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

Hello, World!

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

モデル

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

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

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

ダイス

ここで以下を実装するね

  • ダイスには個数と面の数がある
  • 振ることで乱数生成器から乱数列を作る
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. リデューサー

アクション

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

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 つだね

リデューサー

メデューサ?(難聴

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

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

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

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 コンポーネントたちだよ

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

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. ウェブを描く - やりたい画面を実現するためにコンポーネントをどんどん作った (ファイルを分けようかと思ったけど記事的に見辛くなりそうなのでやめた)

おわりに

ここまで読んでくれた方向けにソースコードがまるっと載った レポジトリ を貼っておきます

Vercel とかでテキトーにデプロイして遊んでくだされ

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5