【24日目】『リーダブルコード』を意識してNext.js/Reactをリファクタリングしてみた
技術ブログ24日目。
本日は、Next.js/Reactの読みやすさを追求します。
〇課題 ショッピングカートの注文確認画面
修正前のコードは、カート内の商品一覧を表示し、小計・消費税・送料・割引を計算して最終的な合計金額を算出しています。計算ロジックと表示ロジックが混ざり合い、非常にメンテナンスがしにくい状態です。
〇修正前コード(動くけれど読みにくいコード)
// src/app/shop/checkout/page.js
"use client";
import { useState, useEffect } from "react";
export default function CheckoutPage() {
const [items, setItems] = useState([]);
const [coupon, setCoupon] = useState("");
useEffect(() => {
// カートの商品を取得(モックデータ)
setItems([
{ id: 1, name: "技術書", price: 3000, quantity: 2 },
{ id: 2, name: "キーボード", price: 12000, quantity: 1 },
]);
}, []);
return (
<div className="p-10">
<h1 className="text-2xl font-bold mb-6">注文確認</h1>
<div className="space-y-4">
{items.map((i) => (
<div key={i.id} className="flex justify-between border-b pb-2">
<span>{i.name} x {i.quantity}</span>
<span>{(i.price * i.quantity).toLocaleString()}円</span>
</div>
))}
</div>
<div className="mt-8 p-4 bg-gray-50 rounded">
{/* ロジックがJSXの中に直接書かれていて、一目で計算内容がわからない */}
<p>小計: {items.reduce((acc, i) => acc + i.price * i.quantity, 0).toLocaleString()}円</p>
<p>消費税 (10%): {Math.floor(items.reduce((acc, i) => acc + i.price * i.quantity, 0) * 0.1).toLocaleString()}円</p>
{/* 送料の判定マジックナンバー */}
<p>送料: {items.reduce((acc, i) => acc + i.price * i.quantity, 0) >= 10000 ? 0 : 500}円
{items.reduce((acc, i) => acc + i.price * i.quantity, 0) < 10000 && (
<span className="text-sm text-red-500"> (あと{(10000 - items.reduce((acc, i) => acc + i.price * i.quantity, 0)).toLocaleString()}円で送料無料)</span>
)}
</p>
{/* 合計金額の計算が巨大すぎてバグの温床 */}
<div className="text-xl font-bold mt-4 border-t pt-2">
合計: {(
items.reduce((acc, i) => acc + i.price * i.quantity, 0) +
Math.floor(items.reduce((acc, i) => acc + i.price * i.quantity, 0) * 0.1) +
(items.reduce((acc, i) => acc + i.price * i.quantity, 0) >= 10000 ? 0 : 500)
).toLocaleString()}円
</div>
</div>
</div>
);
}
〇リファクタリングの指針
名著『リーダブルコード』に基づき、以下の点を意識しました。
一度に一つのタスクを行う
計算(ロジック)と表示(JSX)を分離。
小計、税金、送料を独立した変数で算出し、最後に合計を出す流れに整える。
説明的な変数の導入
何度も登場する items.reduce(...) を subtotal という名前に集約し、コードの意味を明確にする。
マジックナンバーの定数化
税率(0.1)や送料無料ライン(10000)などに名前をつけ、ビジネスルールの変更に強くする。
Custom Hookへの抽出
金額計算のロジックを useCart フックに抽出し、コンポーネントをスッキリさせる。
〇修正後のコード
"use client";
import { useState, useEffect, useMemo } from "react";
// 1. 定数・設定の定義(マジックナンバーを排除)
const TAX_RATE = 0.1;
const FREE_SHIPPING_THRESHOLD = 10000;
const STANDARD_SHIPPING_FEE = 500;
// 2. Custom Hook:useCart(計算とデータの管理)
function useCart() {
const [items, setItems] = useState([]);
useEffect(() => {
// 実際はAPI等から取得
setItems([
{ id: 1, name: "技術書", price: 3000, quantity: 2 },
{ id: 2, name: "キーボード", price: 12000, quantity: 1 },
]);
}, []);
// 小計の計算(reduceを一箇所に集約)
const subtotal = useMemo(() => {
return items.reduce((acc, item) => acc + item.price * item.quantity, 0);
}, [items]);
// 消費税の計算
const tax = useMemo(() => Math.floor(subtotal * TAX_RATE), [subtotal]);
// 送料の計算
const shippingFee = useMemo(() => {
return subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING_FEE;
}, [subtotal]);
// 合計金額の計算
const total = useMemo(() => subtotal + tax + shippingFee, [subtotal, tax, shippingFee]);
// 送料無料までの残り金額
const amountToFreeShipping = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal);
return {
items,
subtotal,
tax,
shippingFee,
total,
amountToFreeShipping,
};
}
// 3. メインコンポーネント(見た目の配置)
export default function CheckoutPage() {
const { items, subtotal, tax, shippingFee, total, amountToFreeShipping } = useCart();
return (
<div className="p-10 max-w-2xl mx-auto">
<h1 className="text-2xl font-bold mb-6 border-b pb-2">注文確認</h1>
{/* 商品一覧 */}
<div className="space-y-4 mb-8">
{items.map((item) => (
<div key={item.id} className="flex justify-between text-gray-700">
<span>{item.name} x {item.quantity}</span>
<span>{(item.price * item.quantity).toLocaleString()}円</span>
</div>
))}
</div>
{/* 金額詳細エリア */}
<div className="p-6 bg-gray-50 rounded-lg space-y-3">
<SummaryRow label="小計" value={subtotal} />
<SummaryRow label={`消費税 (${TAX_RATE * 100}%)`} value={tax} />
<div className="flex justify-between items-center">
<span className="text-gray-600">送料</span>
<div className="text-right">
<span className="font-medium">{shippingFee.toLocaleString()}円</span>
{amountToFreeShipping > 0 && (
<p className="text-xs text-red-500 mt-1">
あと{amountToFreeShipping.toLocaleString()}円で送料無料
</p>
)}
</div>
</div>
<div className="text-xl font-bold mt-4 border-t pt-4 flex justify-between text-blue-900">
<span>合計金額</span>
<span>{total.toLocaleString()}円</span>
</div>
</div>
</div>
);
}
// 4. 表示用サブコンポーネント
function SummaryRow({ label, value }) {
return (
<div className="flex justify-between text-gray-600">
<span>{label}</span>
<span>{value.toLocaleString()}円</span>
</div>
);
}
〇主な修正ポイント
タスクの分離
一つの長い式で行われていた「足し算・税金計算・送料判定」を分割。階段を一段ずつ登るように計算を整理したことで、ミスが起きにくい構造にしました。
重複計算の統合
4回書かれていた items.reduce を subtotal という変数に集約。
パフォーマンス向上と可読性向上を同時に実現しました。
ビジネスルールの定数化
税率や送料ラインを定数化。将来のルール変更時も、定義箇所を一箇所直すだけで安全に修正が完了します。
宣言的なUI(JSXのクリーン化)
JSXから計算ロジックを完全に排除。「何を表示するか」だけが書かれた、デザイン変更に強いコードになりました。
〇参照先
▼公式ドキュメント
▼書籍
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック
以上
Discussion