【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の更新ロジックもシンプルになっています。

〇参照先
▼公式ドキュメント
https://react.dev/learn/choosing-the-state-structure

https://react.dev/learn/passing-props-to-a-component

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

以上

Discussion