🔦

【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 と役割を分けました。修正時に「どこを直せばいいか」が瞬時に判断できます。

〇参照先
▼公式ドキュメント

https://react.dev/learn/choosing-the-state-structure

https://react.dev/learn/reusing-logic-with-custom-hooks

▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則

以上

Discussion