【44日目】『プリンシプル オブ プログラミング』を意識してReact/Next.jsをリファクタリングしてみた
技術ブログ44日目。
本日は、React/Next.jsの読みやすさを追求します。
〇課題
ユーザー設定ダッシュボードの改善
メール通知や表示名を管理するシンプルな画面ですが、1つの関数が「状態の変更」と「メッセージの生成」を同時に行っていたり、コンポーネントが必要以上のデータに依存していたりと、拡張しにくい状態です。
〇修正前コード(動くけれど読みにくいコード)
// src/app/settings/preferences/page.tsx
"use client";
import { useState } from "react";
export default function UserPreferences() {
const [user, setUser] = useState({
id: 1,
name: "Taro Yamada",
email: "taro@example.com",
preferences: {
emailNotify: true,
darkMode: false,
},
lastUpdated: "2026-02-01",
});
// CQS違反:更新(コマンド)とメッセージ生成(クエリ)が混ざっている
const updateSettings = (key: string, value: any) => {
const newUser = {
...user,
preferences: { ...user.preferences, [key]: value },
};
setUser(newUser);
// 更新したあとの状態を元にメッセージを返す(副作用と戻り値の混在)
return `設定の ${key} を ${value} に変更しました。現在の通知は ${newUser.preferences.emailNotify ? "ON" : "OFF"} です。`;
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 直接的なロジックの露出
if (e.target.value.length > 20) {
alert("名前が長すぎます");
return;
}
setUser({ ...user, name: e.target.value });
};
return (
<div className="p-10 max-w-xl mx-auto space-y-8">
<h1 className="text-2xl font-bold">ユーザー設定</h1>
<section className="space-y-4 border-b pb-6">
<label className="block font-medium">表示名</label>
<input
value={user.name}
onChange={handleNameChange}
className="border p-2 w-full rounded"
/>
</section>
{/* インターフェース分離の欠如:子コンポーネントが user オブジェクトを丸ごと受け取っている */}
<SettingToggle
label="ダークモード"
user={user}
onToggle={(val) => {
const msg = updateSettings("darkMode", val);
console.log(msg);
}}
/>
<SettingToggle
label="メール通知"
user={user}
onToggle={(val) => {
const msg = updateSettings("emailNotify", val);
alert(msg);
}}
/>
</div>
);
}
// 子コンポーネント:darkMode しか使わないのに user 全体を受け取っている
function SettingToggle({ label, user, onToggle }: any) {
const isActive = label === "ダークモード" ? user.preferences.darkMode : user.preferences.emailNotify;
return (
<div className="flex justify-between items-center p-4 bg-gray-50 rounded">
<span>{label}</span>
<button
onClick={() => onToggle(!isActive)}
className={`px-4 py-2 rounded ${isActive ? "bg-black text-white" : "bg-gray-300"}`}
>
{isActive ? "ON" : "OFF"}
</button>
</div>
);
}
〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
CQS (Command Query Separation)
「状態を変更するコマンド」と「情報を取得するクエリ」を分離します。関数の役割を一つに絞り、副作用の有無を明確にします。
インターフェース分離の原則
子コンポーネントが不要なデータを知りすぎないようにします。表示に必要な最小限のデータ(isActive: boolean)だけを渡す設計に変えます。
ポカヨケ (Error-proofing)
文字列の打ち間違いを防ぐために、定数やTypeScriptの型を利用して「ミスが起きようのない」仕組みを導入します。
〇修正後コード
"use client";
import { useState } from "react";
// 1. 定数・型定義(ポカヨケ:タイポを防ぎ、型で守る)
const MAX_NAME_LENGTH = 20;
const PREF_KEYS = {
EMAIL_NOTIFY: "emailNotify",
DARK_MODE: "darkMode",
} as const;
type PreferenceKey = typeof PREF_KEYS[keyof typeof PREF_KEYS];
interface UserPreferences {
emailNotify: boolean;
darkMode: boolean;
}
// 2. クエリ関数(CQS:状態を返したりメッセージを作ったりする専門家)
const getPreferenceChangeMessage = (label: string, isActive: boolean): string => {
return `${label} を ${isActive ? "ON" : "OFF"} に変更しました。`;
};
// 3. メインコンポーネント(表示の構成とコマンドの発行)
export default function UserPreferencesPage() {
const [userName, setUserName] = useState("Taro Yamada");
const [prefs, setPrefs] = useState<UserPreferences>({
emailNotify: true,
darkMode: false,
});
// コマンド:状態を更新することだけに集中する(CQS)
const togglePreference = (key: PreferenceKey) => {
setPrefs((prev) => {
const nextValue = !prev[key];
// メッセージが必要なら、更新とは別の場所(クエリ)で生成する
return { ...prev, [key]: nextValue };
});
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextName = e.target.value;
// バリデーション(ポカヨケ)
if (nextName.length > MAX_NAME_LENGTH) return;
setUserName(nextName);
};
return (
<div className="p-10 max-w-xl mx-auto space-y-8">
<h1 className="text-2xl font-bold">ユーザー設定</h1>
<section className="space-y-4 border-b pb-6">
<label className="block font-medium">表示名({MAX_NAME_LENGTH}文字以内)</label>
<input
value={userName}
onChange={handleNameChange}
className="border p-2 w-full rounded shadow-sm focus:ring-2 focus:ring-black outline-none"
/>
</section>
<div className="space-y-4">
{/* インターフェース分離:必要な boolean 値と関数だけを渡す */}
<SettingToggle
label="ダークモード"
isActive={prefs.darkMode}
onToggle={() => {
togglePreference(PREF_KEYS.DARK_MODE);
console.log(getPreferenceChangeMessage("ダークモード", !prefs.darkMode));
}}
/>
<SettingToggle
label="メール通知"
isActive={prefs.emailNotify}
onToggle={() => {
togglePreference(PREF_KEYS.EMAIL_NOTIFY);
alert(getPreferenceChangeMessage("メール通知", !prefs.emailNotify));
}}
/>
</div>
</div>
);
}
// 4. 子コンポーネント(インターフェース分離:受け取るデータを最小限に)
interface ToggleProps {
label: string;
isActive: boolean;
onToggle: () => void;
}
function SettingToggle({ label, isActive, onToggle }: ToggleProps) {
return (
<div className="flex justify-between items-center p-4 bg-gray-50 rounded-lg border border-gray-100">
<span className="font-medium text-gray-700">{label}</span>
<button
onClick={onToggle}
className={`w-20 py-2 rounded-full font-bold transition-colors ${
isActive ? "bg-black text-white" : "bg-gray-300 text-gray-600"
}`}
>
{isActive ? "ON" : "OFF"}
</button>
</div>
);
}
〇主な修正ポイント
CQSの徹底
コマンド(togglePreference)とクエリ(getPreferenceChangeMessage)を分けました。これにより、関数の目的が明確になり、デバッグが容易になります。
インターフェース分離
SettingToggleが「ユーザー」という巨大な概念を知らなくて済むようにしました。受け取るデータを最小限(boolean)にしたことで、他の画面でも使い回せる汎用的な部品になりました。
ポカヨケ(不備の回避)
PREF_KEYS という定数を使うことで、タイポによるバグをコンパイル段階で防げるようにしました。また、Stateの構造を平坦にしたことで、Reactの更新ロジックもシンプルになっています。
〇参照先
▼公式ドキュメント
▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則
以上
Discussion