【20日目】『リーダブルコード』を意識してNext.js/Reactをリファクタリングしてみた
技術ブログ20日目。
本日は、Next.js/Reactの読みやすさを追求します。
〇課題 ランニング記録管理画面の改善
走行データの管理画面です。機能は正しく動きますが、計算式がJSX(HTML部分)の中に直接書かれており、非常に解読しづらい状態になっています。
〇修正前コード(動くけれど読みにくいコード)
src/app/fitness/stats/page.tsx
// src/app/fitness/stats/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function RunStats() {
const [d, setD] = useState<any[]>([]);
const [f, setF] = useState(0); // 最小距離のフィルター
useEffect(() => {
fetch("/api/runs")
.then((res) => res.json())
.then((data) => setD(data));
}, []);
return (
<div className="p-10">
<h1 className="text-2xl mb-4">マイランニングログ</h1>
<input
type="number"
value={f}
onChange={(e) => setF(Number(e.target.value))}
placeholder="最小距離(km)"
className="border p-2 mb-4"
/>
<div className="space-y-4">
{d
.filter((i) => i.dist >= f)
.map((i) => (
<div key={i.id} className="border p-4 rounded shadow">
<p>日付: {i.dt}</p>
<p>距離: {i.dist} km</p>
{/* 複雑な計算がJSXに直接書かれている(ペース計算と判定) */}
<p>
ペース: {(i.dur / i.dist).toFixed(2)} 分/km
{i.dur / i.dist < 5 ? (
<span className="text-green-500"> (速い!)</span>
) : i.dur / i.dist > 7 ? (
<span className="text-red-500"> (ゆっくり)</span>
) : (
<span> (平均的)</span>
)}
</p>
{/* 消費カロリー計算(体重60kgと仮定したマジックナンバー) */}
<p>推定消費カロリー: {Math.floor(i.dist * 60)} kcal</p>
</div>
))}
</div>
</div>
);
}
〇リファクタリングの指針
名著『リーダブルコード』に基づき、以下の点を意識しました。
説明的な変数(要約変数)の導入
計算結果に pace などの名前をつけ、式が何を表しているのか明確にする。
マジックナンバーを排除
カロリー計算に使う体重やペースの判定基準を定数として定義する。
下位の問題を抽出(Custom Hook / コンポーネント分割)
データ管理を useActivityLog へ抽出し、個別の表示を RunCard コンポーネントに分離する。
制御フローを読みやすく整理
三項演算子のネストを排除し、ガード節(if文での早期リターン)を使って整理する。
〇修正後のコード
"use client";
import { useState, useEffect, useMemo } from "react";
// 1. 定数・型定義
const USER_WEIGHT_KG = 60;
const PACE_LIMIT = {
FAST: 5,
SLOW: 7,
};
interface RunRecord {
id: number;
distance: number; // 距離(km)
duration: number; // 時間(分)
date: string;
}
// 2. Custom Hook(データ管理と加工)
function useActivityLog() {
const [logs, setLogs] = useState<RunRecord[]>([]);
const [minDistance, setMinDistance] = useState(0);
useEffect(() => {
fetch("/api/runs")
.then((res) => res.json())
.then((data) => setLogs(data));
}, []);
// フィルター処理をメモ化(パフォーマンス向上)
const filteredLogs = useMemo(() => {
return logs.filter((log) => log.distance >= minDistance);
}, [logs, minDistance]);
return {
filteredLogs,
minDistance,
setMinDistance,
};
}
// 3. メインコンポーネント(全体のレイアウト)
export default function RunStatsPage() {
const { filteredLogs, minDistance, setMinDistance } = useActivityLog();
return (
<div className="p-10 space-y-6">
<header>
<h1 className="text-2xl font-bold">マイランニングログ</h1>
</header>
<section className="bg-gray-50 p-4 rounded">
<label className="block text-sm font-medium mb-1">表示する最小距離 (km)</label>
<input
type="number"
value={minDistance}
onChange={(e) => setMinDistance(Number(e.target.value))}
className="border p-2 rounded w-32"
/>
</section>
<div className="grid gap-4">
{filteredLogs.map((log) => (
<RunCard key={log.id} log={log} />
))}
</div>
</div>
);
}
// 4. サブコンポーネント(個別の記録表示)
function RunCard({ log }: { log: RunRecord }) {
// ペースの計算(分/km)
const pace = log.duration / log.distance;
// 消費カロリーの計算(簡易式:体重 × 距離)
const estimatedCalories = Math.floor(log.distance * USER_WEIGHT_KG);
// ペースに応じたラベルの取得
const getPaceLabel = () => {
if (pace < PACE_LIMIT.FAST) return <span className="text-green-500 font-bold"> (速い!)</span>;
if (pace > PACE_LIMIT.SLOW) return <span className="text-red-500"> (ゆっくり)</span>;
return <span className="text-gray-500"> (平均的)</span>;
};
return (
<div className="border p-4 rounded-lg shadow-sm bg-white flex justify-between items-center">
<div className="space-y-1">
<p className="text-sm text-gray-500">{log.date}</p>
<p className="text-lg font-semibold">{log.distance.toFixed(1)} km</p>
</div>
<div className="text-right space-y-1">
<p>
ペース: {pace.toFixed(2)} 分/km
{getPaceLabel()}
</p>
<p className="text-sm text-gray-600">
消費目安: {estimatedCalories.toLocaleString()} kcal
</p>
</div>
</div>
);
}
〇主な修正ポイント
定数化でメンテナンス性向上
体重やペースの基準値を一箇所にまとめました。将来、体重の設定機能を追加したくなった際も、どこを直せばよいか一目瞭然です。
Custom Hookによる関心の分離
データの取得やフィルタリングを useActivityLog に閉じ込めました。これにより、メインのコンポーネントは見た目のレイアウトだけに集中できます。これを関心の分離と呼びます。
説明的な命名
i.dur / i.dist に pace という名前をつけました。
コードを読み返す際に計算の意図を思い出すコストが激減します。
ガード節でネストを解消
三項演算子のネストを getPaceLabel 関数へ移し、if文で整理しました。上から順に読むだけで条件が把握できる、直線的なコードになりました。
〇参照先
▼公式ドキュメント
▼書籍
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
以上
Discussion
リーダブルコードと直接的には関係ないかもしれませんが、典型的なFetch-on-renderをやってしまっています。
"use client"があるということは、RSCを使用していると思うので、Server Componentでのデータフィッチにするべきかなと思います。
お節介かもしれないですが、少し気になったので...🙏