Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

付録G 完成版サンプルアプリのコード解説

本書を通して育てた Relay の全体像を、ファイルの役割と、対応する章で振り返ります。各部分が「どの章で、なぜそう作ったか」を、まとめて見渡せます。

モデル

ファイル役割章
app/models/user.rbログインユーザー。担当タスク・コメントを持つ第5章
app/models/project.rbタスクをまとめる単位第5章
app/models/task.rb中心リソース。status enum、変更通知の broadcast第5章・第18章
app/models/comment.rb / tag.rb / tagging.rbコメント・タグ(中間モデル含む)第5章

Task#status の enum(todo / in_progress / done)は、検索・絞り込み・バリデーション・表示の軸になりました(第4章)。

コントローラ

app/controllers/tasks_controller.rb が中心です。

  • index … 検索・絞り込み(sanitize_sql_like)、ページネーション、該当総数 @total_count とページ内の @paged_tasks(第23章・第24章・第17章)。
  • create / update / destroy … 成功は redirect / Turbo Streams、失敗は 422(第8章・第16章)。format.turbo_stream と format.html の両方を持つ。

認可はコントローラで行います(第31章)。Relay は単一チーム前提なので最小限です。

ビューと partial

ファイル役割章
app/views/tasks/_task.html.erbタスク 1 件の表示(turbo_frame_tag)第12章
app/views/tasks/_tasks.html.erb一覧領域(空状態の分岐込み)第17章
app/views/tasks/_form.html.erbフォーム(a11y 属性)第25章
app/views/tasks/*.turbo_stream.erbcreate / destroy などの stream 命令第16章
app/views/layouts/_flash.html.erbフラッシュ(#flash / aria-live)第27章

partial を一覧と stream で共通化したのが、設計の要でした(第12章・第17章)。

Stimulus コントローラ

ファイル役割章
autofocus_controller.js要素にフォーカス(エラーサマリにも再利用)第19章・第25章
counter_controller.js文字数カウンタ第20章
toast_controller.jsトーストの自動消滅・閉じる(Values の delay)第21章・第27章
search_controller.js入力の debounce 送信第23章
infinite_scroll_controller.jsIntersectionObserver で自動読み込み第24章
dropdown_controller.js / modal_controller.jsドロップダウン・モーダル第26章
chart_controller.js など外部ライブラリ連携第22章

初期化で確保するタイマー・外部ライブラリには破棄処理を対にし、状態は HTML 側に置きました(第6部)。

リアルタイム

  • turbo_stream_from(購読、ログイン時のみ)と broadcast_*_to(配信)でリアルタイム更新(第18章・第27章)。
  • 配信範囲と認可に注意(第31章)。

読み進め方

このアプリは、第2部で素の CRUD を作り、第3部以降で 1 つずつ Hotwire 化したものです。各ファイルを見るときは、対応する章に戻ると、「なぜそう書いたか」がたどれます。最初の素の状態から、段階を追って育てたことが、コード全体に表れています。

完成版の Relay のコード一式は、次のリポジトリで公開しています。

手元のコードと突き合わせながら読むと、各章の差分が追いやすくなります。

完成版モデルと差分の管理

app/models/task.rb

class Task < ApplicationRecord
  belongs_to :project
  belongs_to :assignee, class_name: "User", optional: true
  has_many :comments, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings

  enum :status, { todo: 0, in_progress: 1, done: 2 }, default: :todo, validate: true

  validates :title, presence: true

  # 一覧条件は受信者ごとに異なるので、行を一律に配信しない。
  after_commit :announce_change
  after_create_commit :notify_assignee
  after_update_commit :notify_assignee, if: :saved_change_to_assignee_id?

  private

  def announce_change
    Turbo::StreamsChannel.broadcast_update_to :tasks, target: "task_changes",
      partial: "tasks/change"
  end

  def notify_assignee
    return unless assignee
    Turbo::StreamsChannel.broadcast_append_to assignee, target: "toasts",
      partial: "toasts/toast", locals: { message: "新しいタスクが割り当てられました。" }
  end
end

本文で掲載する完成版コードは manuscript/snippets/ に収録しています。scripts/sync_sample_snippets.py --source /path/to/Relay --check でサンプルとの一致を確認できます。更新時は --check を外して同期し、原稿の説明とテストも確認します。

第13章のサイドバー、第18章前半の行配信、第30章のキャッシュは設計を比較する別案です。完成版へすべて重ねて追加するものではありません。Native の最小構成は付録Hに分離し、Web アプリの検証と実機検証を区別します。