← 解説「部品を切る基準 — いつコンポーネントに分けるか」に戻る
サンプルコードで身につける: 部品を切る基準 — いつコンポーネントに分けるか
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1基準①: 2箇所以上で使うなら切る
同じ商品カードのHTMLが一覧ページとおすすめ欄にコピペされている状態がBeforeです。部品に切り出せば修正が1箇所で済み、2つの画面の見た目がずれる事故もなくなります。
// ── Before: 同じHTMLが2つのコンポーネントにコピペされている ──
// product-list-page.html にも recommend-section.html にも
// <div class="card"><img ...><h3>{{ p.name }}</h3>...</div>
// が丸ごと書かれている → 片方だけ直して見た目がずれる事故が起きる
// ── After: ProductCard として切り出し、両方から使う ──
import { Component, input } from '@angular/core';
import { Product } from './product.model';
@Component({
selector: 'app-product-card',
template:
'<div class="card">' +
' <h3>{{ product().name }}</h3>' +
' <p>{{ product().price }}円</p>' +
'</div>',
})
export class ProductCard {
product = input.required<Product>();
}
// 一覧ページもおすすめ欄も <app-product-card [product]="p" /> の1行になる
2基準②: 名前が付けられるなら部品の資格がある
「評価スター」のように単体で名前が付く塊は、切り出すと責務がはっきりします。逆に「一覧の上のほうのdiv」としか呼べない塊は、まだ部品として独立させる段階ではありません。
// 名前が付く = 部品になる例
import { Component, input } from '@angular/core';
// 「評価スター」— 何をする部品か名前だけで伝わる
@Component({
selector: 'app-rating-stars',
template: '<span>{{ stars() }}</span>',
})
export class RatingStars {
// 5段階評価(1〜5)を受け取って★で表示する
rating = input.required<number>();
stars() {
return '★'.repeat(this.rating()) + '☆'.repeat(5 - this.rating());
}
}
// 名前が付かない例:
// 「検索ボックスと件数表示が並んでいるあの辺」
// → うまく名前が付かないうちは、無理に切らず親の中に置いておく
3基準③: 親の事情を知らない部品にする
Beforeの部品は商品一覧という親の事情に踏み込んでおり、他の画面で使えません。Afterは「数値を受け取り変更を通知する」だけなので、商品でも店舗でもレビューでも使い回せます。
import { Component, inject, input, output } from '@angular/core';
import { ProductService } from './product.service';
// ── Before: 親の事情(商品サービス)を知ってしまっている ──
@Component({ selector: 'app-rating-bad', template: '...' })
export class RatingBad {
private products = inject(ProductService); // 商品専用になってしまう
productId = input.required<number>();
onSelect(value: number) {
this.products.updateRating(this.productId(), value); // 保存まで自分でやる
}
}
// ── After: Input/Output だけで完結する ──
@Component({ selector: 'app-rating', template: '...' })
export class Rating {
value = input.required<number>(); // 表示する値を受け取る
valueChange = output<number>(); // 変更を親に知らせるだけ
onSelect(value: number) {
this.valueChange.emit(value); // 保存は親の仕事。自分は通知だけ
}
}
4切りすぎの害 — 1画面5ファイルの迷路
基準に当てはまらない数行のHTMLまで部品化すると、単純な画面を追うのに何ファイルも行き来することになります。部品化はタダではなく、ファイルを渡り歩く追跡コストが付いてきます。
// ── 切りすぎの例: どれも1箇所でしか使わない数行の部品 ──
//
// profile-page.ts
// └─ <app-profile-header /> … h1が1行あるだけ
// └─ <app-profile-avatar /> … imgが1枚あるだけ
// └─ <app-profile-name /> … spanが1個あるだけ
// └─ <app-profile-footer /> … pが1行あるだけ
//
// 「名前の表示を直したい」→ どのファイル?と探し回る羽目になる
// ── ちょうどよい例: まず1つのコンポーネントで育てる ──
//
// profile-page.ts に素直にHTMLを書く。
// その後「アバターを他の画面でも使いたい」となった時点で
// ProfileAvatar だけを切り出せばよい(基準①を満たした瞬間に切る)
5迷ったときの判定フローを1枚にまとめる
3つの基準を順に確かめるだけの単純なフローです。2つ以上当てはまったら切り出し、そうでなければ親の中で育てます。「後から切り出す」は設計の失敗ではなく、正しい育て方です。
// ── コンポーネント分割の判定フロー ──
//
// Q1. 2箇所以上で使う?(近い将来の見込みも含む)
// はい → +1点
// Q2. 単体で名前が付く?(「商品カード」「ページ送り」など)
// はい → +1点
// Q3. Input/Output だけで完結し、親の事情を知らずに済む?
// はい → +1点
//
// 2点以上 → 切り出す
// 1点以下 → まだ切らない。親の中に置いたまま育てる
//
// ポイント:
// ・最初から切らなかったことは失敗ではない
// ・基準を満たした「その時」に切り出せば十分間に合う