【40日目】『プリンシプル オブ プログラミング』を意識してReact/Next.jsをリファクタリングしてみた
技術ブログ40日目。
本日は、React/Next.jsの読みやすさを追求します。
〇課題
通知センター・リスト画面の改善
一見正しく動作していますが、将来の拡張を予想しすぎた不要な機能(YAGNI違反)や、UIがデータの詳細を知りすぎている(カプセル化不足)など、メンテナンス性が低い状態の通知リストを改善します。
〇修正前コード(動くけれど読みにくいコード)
// src/app/notifications/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function NotificationPage() {
const [data, setData] = useState<any[]>([]);
// YAGNI違反:将来、通知をマルチセレクトして一括削除するかもしれないから...
// と用意された、現状使われていない複雑な状態管理
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [isBulkMode, setIsBulkMode] = useState(false);
const [futureFilter, setFutureFilter] = useState("all");
useEffect(() => {
// 通知データの取り寄せ
fetch("/api/notifications")
.then((res) => res.json())
.then((json) => setData(json));
}, []);
return (
<div className="p-6 max-w-2xl mx-auto">
<h1 className="text-xl font-bold mb-4">通知センター</h1>
<div className="space-y-3">
{data.map((item) => (
<div key={item.id} className="p-4 border rounded-lg shadow-sm">
<div className="flex justify-between">
{/* カプセル化不足:データの内部構造(typeの文字列)をUIが直接判定している */}
<span className="text-xs font-bold px-2 py-1 rounded bg-gray-100">
{item.type === "INFO" ? "お知らせ" : item.type === "ALERT" ? "警告" : "重要"}
</span>
{/* DRY違反:日付のフォーマット処理があちこちに散らばっている */}
<span className="text-xs text-gray-400">
{new Date(item.createdAt).toLocaleDateString("ja-JP")}
</span>
</div>
<h2 className="mt-2 font-medium">{item.title}</h2>
{/* 複雑な条件分岐:一度に複数のことを判定している */}
<div className="mt-4 flex gap-2">
<button className="text-sm text-blue-600">
{item.link ? "詳細を見る" : "閉じる"}
</button>
{/* YAGNI違反:将来、SNSシェア機能をつけるかもしれないから...と置かれた未実装ボタン */}
<button className="text-sm text-gray-400" onClick={() => alert("未実装です")}>
シェア
</button>
</div>
</div>
))}
</div>
</div>
);
}
〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
YAGNI (You Ain't Gonna Need It):
「今、必要なこと」だけに集中。使っていないStateや未実装ボタンを削除し、コードを軽量に保ちます。
DRY (Don't Repeat Yourself):
日付形式やラベル表示など、重複するロジックを共通化します。
カプセル化 (Encapsulation):
UIがAPIの生データ(typeの文字列など)に依存しないよう、表示用加工を隠蔽します。
KISS (Keep It Simple, Stupid):
複雑な三項演算子を整理し、一瞬で理解できる単純な構造にします。
〇修正後コード
"use client";
import { useState, useEffect, useMemo } from "react";
// 1. 定数・型定義(カプセル化の準備)
const NOTIFICATION_LABELS = {
INFO: { label: "お知らせ", style: "bg-gray-100 text-gray-800" },
ALERT: { label: "警告", style: "bg-red-100 text-red-800" },
URGENT: { label: "重要", style: "bg-orange-100 text-orange-800" },
} as const;
type NotificationType = keyof typeof NOTIFICATION_LABELS;
interface NotificationItem {
id: number;
type: NotificationType;
title: string;
createdAt: string;
link?: string;
}
// 2. ヘルパー関数(DRY:共通ロジックの抽出)
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ja-JP");
};
// 3. Custom Hook(SRP:データの取得と管理に専念)
function useNotifications() {
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
useEffect(() => {
// 実際はAPI等から取得
setNotifications([
{ id: 1, type: "INFO", title: "システムメンテナンスのお知らせ", createdAt: "2026-02-10T10:00:00Z", link: "/news/1" },
{ id: 2, type: "ALERT", title: "パスワードの有効期限が近づいています", createdAt: "2026-02-09T15:30:00Z" },
]);
}, []);
return { notifications };
}
// 4. メインコンポーネント(表示の構成:KISS)
export default function NotificationPage() {
const { notifications } = useNotifications();
return (
<div className="p-6 max-w-2xl mx-auto space-y-6">
<header>
<h1 className="text-2xl font-bold text-gray-900">通知センター</h1>
</header>
<div className="space-y-4">
{notifications.map((item) => (
<NotificationCard key={item.id} item={item} />
))}
</div>
</div>
);
}
// 5. サブコンポーネント(カプセル化:表示ロジックを隠蔽)
function NotificationCard({ item }: { item: NotificationItem }) {
const config = NOTIFICATION_LABELS[item.type];
return (
<div className="p-4 border border-gray-200 rounded-xl shadow-sm hover:shadow-md transition bg-white">
<div className="flex justify-between items-start mb-3">
<span className={`text-xs font-bold px-2.5 py-1 rounded-full ${config.style}`}>
{config.label}
</span>
<time className="text-xs text-gray-500">
{formatDate(item.createdAt)}
</time>
</div>
<h2 className="text-gray-800 font-semibold leading-tight">{item.title}</h2>
<div className="mt-4">
<NotificationAction link={item.link} />
</div>
</div>
);
}
function NotificationAction({ link }: { link?: string }) {
if (!link) {
return (
<button className="text-sm font-medium text-gray-400 cursor-default">
詳細なし
</button>
);
}
return (
<a href={link} className="text-sm font-medium text-blue-600 hover:text-blue-800">
詳細を見る
</a>
);
}
〇主な修正ポイント
YAGNIの徹底:
「いつか使うかも」で用意されていた isBulkMode や未実装ボタンを削除。コードが軽くなり、バグの潜む場所を減らしました。
DRYの適用:
日付フォーマットを関数化。表示形式を一括変更したい際、この一箇所を直すだけで済む堅牢な設計にしました。
カプセル化による情報隠蔽:
NOTIFICATION_LABELS 定数に色と文言を閉じ込めました。
単一責任の原則 (SRP):
データ取得はHook、カード表示は NotificationCard、ボタン出し分けは NotificationAction と役割を分けました。修正時に「どこを直せばいいか」が瞬時に判断できます。
〇参照先
▼公式ドキュメント
▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則
以上
Discussion