🧩 Dual Logic とは何か
SPA(React/Vue/Angular + Rails API など)の典型構成では、同じ業務ルールを2つの言語で二重実装しやすくなります。
- バックエンド(Rails/Ruby): セキュリティとデータ整合性のために必須(最終的な真実)
- フロントエンド(JavaScript/TypeScript): 即時フィードバックと操作感のために実装されがち
この「ネットワーク境界をまたいだDRY違反」が、Dual Logic(ロジック重複)の本質です。
🔁 Dual Logic が発生しやすい典型パターン
✅ バリデーション(Validation)
- サーバ: DB を守るために Rails で検証する
- クライアント: UX のために JS/TS でも同様の検証をする
破綻パターン:
サーバ側のルールを更新したのに、フロント側が追従できず「入力時はOK、送信したらNG」になる。
# backend
validates :email, presence: true, uniqueness: true// frontend(例)
const ok = /.+@.+\..+/.test(email) // uniqueness や正規化は再現できない
🛂 認可(Authorization / Permission)
- サーバ:
current_user.admin?や Pundit/Cancancan で最終判断 - クライアント:
isAdmin等でボタン表示を制御
ここで重要なのは、クライアントの判定はセキュリティではなく「表示制御」に過ぎないことです。
しかし二重管理になる以上、UI と権限制御がズレると混乱が増えます。
# backend
raise Pundit::NotAuthorizedError unless policy(@post).update?// frontend
{isAdmin && <EditButton />} // UX目的。権限の真実はサーバ側🧮 表示整形(日時/通貨/ロケール)
- サーバ: Rails helper / i18n
- クライアント: Intl / 日付ライブラリ
破綻パターン:
タイムゾーン、丸め、通貨表示の微差が積み上がり「同じ値なのに表示が違う」問題になる。
🧪 例:サインアップフォームが Logic #1 #2 #3 に増殖する
SPA のフォーム実装は次のようになりやすいです。
- 入力中に JS の正規表現でエラー表示(Logic #1)
- 送信後に Rails 側で uniqueness や正規化、制約チェック(Logic #2)
- Rails から返った JSON エラーを、フロントで項目ごとにマッピング(Logic #3)
特に #3 の “エラー変換層” が、型・i18n・複数エラー・例外系・仕様変更を抱えて肥大化しやすいポイントです。
💥 Dual Logic の本当のコスト(バグ + セキュリティ認知の歪み)
Dual Logic は「コードが増える」以上に、破綻モードが増えるのが痛いです。
- UI では通るのにサーバで弾かれる → UX 劣化、問い合わせ増
- UI が勝手にブロックする → 機能が存在しないように見える
- UI 側の条件分岐が “権限” と誤解される → セキュリティ意識の低下
そしてテストは増殖します。
- バックエンドテスト(必須)
- フロントエンドテスト(必須)
- 契約テスト(できれば)
- shared schema / types の運用(できれば)

⚡ Hotwire の賭け:ロジックはサーバに集約し、体感は速くする
Hotwire は “HTML Over The Wire” の思想で、次の転換をします。
- SPA: JSON を送ってクライアントが描画する
- Hotwire: 描画済みの HTML を送って差し替える
Turbo Drive は、リンククリックやフォーム送信をインターセプトして HTML を fetch し、ページ遷移を高速化します。
Turbo Frames は、ページの一部だけを独立して差し替えます。
結果として、業務ルールは Rails に1回だけ書き、UI はサーバの再描画 HTML を表示するだけになります。
🧱 Hotwire コンポーネント別:Dual Logic を削る仕組み
🚗 Turbo Drive(クライアントルーティングの重複を減らす)
- ナビゲーションを intercept
- HTML をバックグラウンドで取得して反映
🖼️ Turbo Frames(描画ロジックの重複を減らす)
- フォームの成功/失敗をサーバ側で通常通り render
- 対象 Frame だけ差し替える
📡 Turbo Streams(状態同期ロジックの重複を減らす)
<turbo-stream>で HTML fragment を配信- WebSocket/SSE などで push も可能
🌿 Stimulus(必要最小の JS に限定)
- toggle / clipboard など “ブラウザでしかできない振る舞い” のみ
- 業務ルールや状態管理の本丸には踏み込まない

🛠️ バリデーションは1回だけ。エラー表示は HTML 差し替え
🧠 Single Source of Truth
# app/models/task.rb
class Task < ApplicationRecord
validates :title, presence: true, length: { minimum: 3 }
end🖼️ Frame で対象範囲を囲む
<!-- app/views/tasks/_task.html.erb -->
<%= turbo_frame_tag dom_id(task) do %>
<div class="task-card">
<strong><%= task.title %></strong>
<%= link_to "Edit", edit_task_path(task) %>
</div>
<% end %>🎛️ 422 を返すのが重要
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
def edit
@task = Task.find(params[:id])
end
def update
@task = Task.find(params[:id])
if @task.update(task_params)
redirect_to @task
else
# Turbo のフォームフローでは、失敗時に 422 を返すのが定石
render :edit, status: :unprocessable_entity
end
end
private
def task_params
params.require(:task).permit(:title)
end
endTurbo はフォーム送信後に redirect を期待し、エラー時は 4xx/5xx(代表例: 422)で「例外ルート」としてレンダリングを扱う設計議論があります。
✍️ 同じフォームを再レンダリングするだけ
<!-- app/views/tasks/edit.html.erb -->
<%= turbo_frame_tag dom_id(@task) do %>
<%= form_with(model: @task) do |form| %>
<% if @task.errors.any? %>
<div class="error-message">
<%= @task.errors.full_messages.to_sentence %>
</div>
<% end %>
<%= form.text_field :title %>
<%= form.submit "Save" %>
<%= link_to "Cancel", task_path(@task) %>
<% end %>
<% end %>ポイント:
ブラウザは「なぜ失敗したか」を知らなくていい。サーバが生成した HTML を表示するだけです。
📏 データ
⏱️ 1) HTML と JSON の生成コスト比較
Flask で 1000 件を描画した比較では、テンプレート HTML の生成が JSON より短いケースが示されています(おおよそ HTML ~7ms、JSON ~12ms)。
「JSON は常に軽い/速い」という思い込みを崩す材料になります。
📦 2) 圧縮で “HTML は重い” 議論が縮む
HTML と JSON の圧縮後サイズの比較を扱う資料や実験があり、gzip 等で差が小さくなるケースが観測されています。
🏗️ 3) Hotwire は大規模プロダクトでも採用されている
Basecamp/HEY 文脈で Hotwire をプロダクションへ導入する話が公開されています。
- 採用の狙い: SPA 的な操作感を維持しつつ、複雑さと二重実装(Dual Logic)を増やさないため
- やったこと: JSON + クライアント描画中心ではなく、サーバが HTML を返し、Turbo が差し替える方式を中核にした(HTML over the wire)
- 示唆(議論の結論):
「大規模になったら SPA が必須」という前提に対して、“サーバ中心 + 少量の JS” でも十分にリッチで運用できるという実例になっている - 実務的な意味:
フロントとバックでロジックを二重に持つ必要が減り、保守・実装・テストの総量を抑えやすい(チーム規模や開発速度の観点でも説明されている)
📊 まとめ:SPA と Hotwire を比較するなら「速度」より「二重化の量」を見る
| 観点 | ⚡ Hotwire(Single Logic) | 🧩 SPA(Dual Logic) |
|---|---|---|
| バリデーション | Rails に集約 | Rails + JS の二重化 |
| エラー表示 | HTML 再描画 | JSON → UI マッピング |
| ルーティング | Turbo Drive | Client Router |
| 状態同期 | Streams/partial | Store/reconciliation |
| オフライン | 弱い(標準) | 強い(設計次第) |
| 保守コスト | 低め | 高め |

Hotwire の価値は「HTML が速い」よりも、業務ロジックの単一性(Single Source of Truth)を保てる点にあります。Dual Logic を避けられるだけで、開発と運用の事故率が落ちます。
