【32日目】『プリンシプル オブ プログラミング』を意識してNext.js/Reactをリファクタリングしてみた
技術ブログ32日目。
本日は、Next.js/Reactの読みやすさを追求します。
〇課題
ユーザープロフィール管理画面の改善
ユーザー情報を取得し、そのデータに基づいて「会員ランク」を判定、さらにバリデーションと表示をすべて1つのコンポーネントで行っています。細かい文字列操作(低レベル)と画面レイアウト(高レベル)が混ざり合っており、何をしているか把握するのに時間がかかる状態です。
〇修正前コード(動くけれど読みにくいコード)
// src/app/profile/settings/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function UserSettings() {
const [user, setUser] = useState<any>(null);
const [email, setEmail] = useState("");
useEffect(() => {
// ユーザーデータの取得
fetch("/api/user")
.then((res) => {
if (res.status === 200) {
return res.json();
}
})
.then((data) => {
setUser(data);
setEmail(data.email);
});
}, []);
const handleSave = () => {
// バリデーションの低レベルなロジックがここに混ざっている
if (!email || email.indexOf("@") === -1 || email.length < 5) {
alert("不適切なメールアドレスです");
return;
}
// 保存処理
console.log("Saving...", email);
};
if (!user) return <div>Loading...</div>;
// 会員ランクの判定ロジック(SLAPに反している)
let rankName = "一般";
let rankColor = "text-gray-500";
if (user.isPremium) {
if (user.points > 10000) {
rankName = "ダイヤモンド";
rankColor = "text-blue-600";
} else {
rankName = "プレミアム";
rankColor = "text-green-600";
}
}
return (
<div className="p-8 space-y-4">
<h1 className="text-xl font-bold">アカウント設定</h1>
<div className="border p-4 rounded">
<p>現在のランク: <span className={rankColor}>{rankName}</span></p>
<p>保有ポイント: {user.points} pt</p>
</div>
<div className="flex flex-col space-y-2">
<label>メールアドレス</label>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 rounded"
/>
</div>
<button
onClick={handleSave}
className="bg-black text-white px-4 py-2 rounded"
>
設定を保存
</button>
</div>
);
}
〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
-
SLAP (Single Level of Abstraction Principle)
「ランク判定の条件」という詳細なロジックと、「画面レイアウト」という全体像を混ぜず、抽象化レベルを統一します。 -
SRP (Single Responsibility Principle)
「データ取得」「バリデーション」「表示」の責任を、Hookや関数に適切に分割します。 -
マジックナンバーの排除
しきい値(10000)を定数化し、コードにドキュメントとしての役割を持たせます。
〇修正後のコード
"use client";
import { useState, useEffect, useMemo } from "react";
// 1. 定数・型定義(マジックナンバーの排除)
const RANK_THRESHOLDS = {
DIAMOND: 10000,
};
interface User {
id: number;
email: string;
points: number;
isPremium: boolean;
}
// 2. ドメインロジック
// メールアドレスの形式チェック
const isValidEmail = (email: string): boolean => {
const minLength = 5;
return email.includes("@") && email.length >= minLength;
};
// 会員ランクの判定(計算ロジックの抽出)
const getMemberRank = (user: User) => {
if (!user.isPremium) {
return { name: "一般", color: "text-gray-500" };
}
const isDiamond = user.points > RANK_THRESHOLDS.DIAMOND;
return isDiamond
? { name: "ダイヤモンド", color: "text-blue-600" }
: { name: "プレミアム", color: "text-green-600" };
};
// 3. Custom Hook(データの取得と状態管理:SRP)
function useUserProfile() {
const [user, setUser] = useState<User | null>(null);
const [email, setEmail] = useState("");
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => {
setUser(data);
setEmail(data.email);
});
}, []);
const saveProfile = (newEmail: string) => {
if (!isValidEmail(newEmail)) {
alert("不適切なメールアドレスです");
return;
}
console.log("Saving...", newEmail);
};
const memberRank = useMemo(() => {
return user ? getMemberRank(user) : null;
}, [user]);
return { user, email, setEmail, saveProfile, memberRank };
}
// 4. メインコンポーネント(表示の構成に集中:SLAP)
export default function UserSettingsPage() {
const { user, email, setEmail, saveProfile, memberRank } = useUserProfile();
if (!user || !memberRank) return <div>Loading...</div>;
return (
<div className="p-8 space-y-6 max-w-md">
<h1 className="text-2xl font-bold">アカウント設定</h1>
<RankInfoCard
rankName={memberRank.name}
rankColor={memberRank.color}
points={user.points}
/>
<div className="flex flex-col space-y-2">
<label className="font-medium">メールアドレス</label>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 rounded shadow-sm"
/>
</div>
<button
onClick={() => saveProfile(email)}
className="w-full bg-blue-600 text-white py-2 rounded font-bold hover:bg-blue-700 transition"
>
設定を保存
</button>
</div>
);
}
// 5. サブコンポーネント
function RankInfoCard({ rankName, rankColor, points }: { rankName: string, rankColor: string, points: number }) {
return (
<div className="border p-4 rounded-lg bg-gray-50">
<p className="text-sm text-gray-600">現在のランク</p>
<p className={`text-xl font-bold ${rankColor}`}>{rankName}</p>
<p className="text-sm text-gray-500 mt-2">保有ポイント: {points.toLocaleString()} pt</p>
</div>
);
}
〇主な修正ポイント
SLAPの導入: 修正後は、メインコンポーネントが isValidEmail などの関数を呼び出すだけになりました。詳細な「どうやるか」が隠され、「何をしているか」という物語のように読めるようになります。
SRPの徹底: Hookはデータ、関数はビジネスルール、コンポーネントは見た目、と責任を明確に分離しました。
ガード節による平坦化: ランク判定のネストを解消し、上から下へ直線的に理解できるロジックに整えました。
〇参照先
▼公式ドキュメント
▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則
以上
Discussion