開発日記⑫ 画面を5つのコンポーネントに分割する

前回までの進捗

前回は、入力ミスをそのまま計算しないように、バリデーション処理を追加した。
ここまで機能を増やしてきたことで、最初は小さかった画面もかなり要素が増えてきた。
現在の画面には、

  • 4人の持ち点入力
  • 自分の席や目標順位の設定
  • 難易度とランダム生成
  • 必要なロン・ツモ点の表示
  • 仮の終局結果入力
  • 終局後の順位表示

などがある。
これらをすべてApp.tsxへ書いていくと、かなり長くなってしまう。
そこで今回は、画面を役割ごとのReactコンポーネントに分割した。

5つのコンポーネントに分ける

現在は、画面を次の5つに分けている。

ScoreInputSection
ConditionSection
RequiredPointsSection
ResultInputSection
ResultSummarySection

役割はそれぞれ、

ScoreInputSection
→ 現在の持ち点を入力する

ConditionSection
→ 自分の席・目標順位・本場・供託などを設定する

RequiredPointsSection
→ 目標達成に必要な点数を表示する

ResultInputSection
→ ロン・ツモ・流局の仮結果を入力する

ResultSummarySection
→ 終局後の点数・順位・目標達成結果を表示する

という形。
画面上でもカードごとに分かれていたので、その単位でコンポーネントにした。

App.tsxからコンポーネントを呼び出す

App.tsxでは、それぞれをimportする。

import ConditionSection
  from "./components/ConditionSection";
import RequiredPointsSection
  from "./components/RequiredPointsSection";
import ResultInputSection
  from "./components/ResultInputSection";
import ResultSummarySection
  from "./components/ResultSummarySection";
import ScoreInputSection
  from "./components/ScoreInputSection";

そして画面を組み立てる部分では、

<ScoreInputSection ... />

<ConditionSection ... />

<RequiredPointsSection ... />

<ResultInputSection ... />

<ResultSummarySection ... />

という形で並べている。
App.tsxに直接大量のHTMLを書くのではなく、

点数入力
↓
条件設定
↓
必要点数
↓
終局結果入力
↓
結果表示

という画面全体の構造が見えやすくなった。

値はpropsで渡す

コンポーネントを分けると、それぞれの画面で必要な値を渡す必要がある。
たとえば持ち点入力では、

<ScoreInputSection
  scores={scores}
  kyotaku={kyotaku}
  onChangeScore={updateScore}
/>

としている。
ScoreInputSection側では、受け取る値の型を定義する。

type ScoreInputSectionProps = {
  scores: Scores;
  kyotaku: number;
  onChangeScore:
    (seat: Seat, value: string) => void;
};

ここでは、

scores
→ 現在の4人分の点数

kyotaku
→ 現在の供託本数

onChangeScore
→ 点数が変更されたときに呼ぶ処理

を親コンポーネントから受け取っている。

state自体はApp側で使う

画面をコンポーネントに分けたからといって、それぞれで同じstateを作り直しているわけではない。
ゲーム全体で使う状態は、前回までに作ったuseGameStateから取得する。

const {
  scores,
  selfSeat,
  goal,
  difficulty,
  honba,
  kyotaku,
  resultType,
  winner,
  loser,
  // ...
} = useGameState();

それを、必要なコンポーネントへ渡していく。
たとえばConditionSectionには、

<ConditionSection
  difficulty={difficulty}
  selfSeat={selfSeat}
  goal={goal}
  honba={honba}
  kyotaku={kyotaku}
  onChangeDifficulty={setDifficulty}
  onChangeSelfSeat={setSelfSeat}
  onChangeGoal={setGoal}
  onChangeHonba={setHonba}
  onChangeKyotaku={setKyotaku}
  onRandomize={randomizeCondition}
  onReset={resetSavedState}
/>

という形で渡している。
条件設定に関係する値と処理だけをまとめて渡す。

表示だけするコンポーネントもある

すべてのコンポーネントが入力を持っているわけではない。
RequiredPointsSectionは、計算結果を表示する役割が中心。

<RequiredPointsSection
  currentSelfRank={currentSelfRank}
  difficulty={difficulty}
  hasInvalidInput={hasInvalidInput}
  alreadyAchieved={alreadyAchieved}
  minimumRequiredWinPoints={
    minimumRequiredWinPoints
  }
  requiredRonList={requiredRonList}
  requiredTsumo={requiredTsumo}
/>

必要なロン点やツモ点の計算そのものは、コンポーネントの中では行わない。
計算済みの結果を受け取って、

現在順位
最低和了点
各家からの必要ロン点
必要ツモ点

などを表示する。
画面側に計算処理をできるだけ持たせないようにした。

入力と計算と表示を分ける

ここまでの整理で、少しずつ役割が分かれてきた。

useGameState
→ 入力状態を管理する

useGameDerivedState
→ 入力値から結果を計算する

各Sectionコンポーネント
→ 入力欄や結果を表示する

App.tsx
→ それらをつなぐ

という構成。
最初からこの形を考えていたわけではなく、機能を追加してコードが増えたので後から分けていった。
小さいうちはApp.tsxだけでも特に困らなかったけれど、入力項目や計算結果が増えてくると、役割ごとに分かれている方が探しやすくなった。

コンポーネントを分ける単位

今回迷ったのが、「どこまで細かくコンポーネントにするか」だった。
たとえば、

点数入力欄1個

だけでもコンポーネントにはできる。
ただ、細かくしすぎるとファイル数やpropsの受け渡しが増える。
今回は、画面上のカード単位で、

現在点数
条件
必要点数
終局結果
結果まとめ

というまとまりがあったので、その単位で分けた。
今の規模ならこのくらいが扱いやすそう。
さらに同じUIを何度も使うようになったら、その部分だけ追加でコンポーネント化することもできる。

App.tsxもまだ小さくはない

コンポーネントへ分けたことでJSX部分は整理できたものの、App.tsxではまだかなり多くの値を受け渡している。
特にConditionSectionやResultInputSectionはpropsが多い。
なので、これで完全に整理できたというわけではない。
ただ、

どの値を管理するか
どの値を計算するか
どの画面で表示するか

が分かれただけでも、以前よりコードを追いやすくなった。
今後さらに画面が増えてきたら、その時点でまた構成を考えることになりそう。

今回できたこと

今回の進捗は以下のとおり。

  • 画面を5つのコンポーネントへ分割した
  • 持ち点入力をScoreInputSectionへ分けた
  • 条件設定をConditionSectionへ分けた
  • 必要点数表示をRequiredPointsSectionへ分けた
  • 終局結果入力をResultInputSectionへ分けた
  • 終局結果表示をResultSummarySectionへ分けた
  • stateやイベント処理をpropsで渡す形にした
  • App.tsxを各処理と画面をつなぐ役割に寄せた

今回も、新しい計算機能そのものを追加したわけではない。
ただ、今後さらに機能を増やしていくために、画面側のコードを整理できた。
最初は一つのファイルに書いていたものが、機能追加とともに少しずつ分かれていくのを見ると、アプリ自体もだいぶ大きくなってきた感じがする。

リファクタリング地獄。牛一。

コメント

タイトルとURLをコピーしました