Dev Study
← 解説「部品を切る基準 — いつコンポーネントに分けるか」に戻る

サンプルコードで身につける: 部品を切る基準 — いつコンポーネントに分けるか

解説で学んだ概念を、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点以下 → まだ切らない。親の中に置いたまま育てる
//
// ポイント:
// ・最初から切らなかったことは失敗ではない
// ・基準を満たした「その時」に切り出せば十分間に合う