🧩 Dual Logic(ロジック重複)という構造コストと Hotwire の解決

🧩 Dual Logic(ロジック重複)という構造コストと Hotwire の解決

· · 5分で読めます

Advertisement

🧩 Dual Logic とは何か

SPA(React/Vue/Angular + Rails API など)の典型構成では、同じ業務ルールを2つの言語で二重実装しやすくなります。

この「ネットワーク境界をまたいだDRY違反」が、Dual Logic(ロジック重複)の本質です。


🔁 Dual Logic が発生しやすい典型パターン

✅ バリデーション(Validation)

破綻パターン:
サーバ側のルールを更新したのに、フロント側が追従できず「入力時はOK、送信したらNG」になる。

# backend
validates :email, presence: true, uniqueness: true
// frontend(例)
const ok = /.+@.+\..+/.test(email) // uniqueness や正規化は再現できない

🛂 認可(Authorization / Permission)

ここで重要なのは、クライアントの判定はセキュリティではなく「表示制御」に過ぎないことです。
しかし二重管理になる以上、UI と権限制御がズレると混乱が増えます。

# backend
raise Pundit::NotAuthorizedError unless policy(@post).update?
// frontend
{isAdmin && <EditButton />} // UX目的。権限の真実はサーバ側

🧮 表示整形(日時/通貨/ロケール)

破綻パターン:
タイムゾーン、丸め、通貨表示の微差が積み上がり「同じ値なのに表示が違う」問題になる。


🧪 例:サインアップフォームが Logic #1 #2 #3 に増殖する

SPA のフォーム実装は次のようになりやすいです。

  1. 入力中に JS の正規表現でエラー表示(Logic #1)
  2. 送信後に Rails 側で uniqueness や正規化、制約チェック(Logic #2)
  3. Rails から返った JSON エラーを、フロントで項目ごとにマッピング(Logic #3)

特に #3 の “エラー変換層” が、型・i18n・複数エラー・例外系・仕様変更を抱えて肥大化しやすいポイントです。


💥 Dual Logic の本当のコスト(バグ + セキュリティ認知の歪み)

Dual Logic は「コードが増える」以上に、破綻モードが増えるのが痛いです。

そしてテストは増殖します。


⚡ Hotwire の賭け:ロジックはサーバに集約し、体感は速くする

Hotwire は “HTML Over The Wire” の思想で、次の転換をします。

Turbo Drive は、リンククリックやフォーム送信をインターセプトして HTML を fetch し、ページ遷移を高速化します。
Turbo Frames は、ページの一部だけを独立して差し替えます。

結果として、業務ルールは Rails に1回だけ書き、UI はサーバの再描画 HTML を表示するだけになります。


🧱 Hotwire コンポーネント別:Dual Logic を削る仕組み

🚗 Turbo Drive(クライアントルーティングの重複を減らす)

🖼️ Turbo Frames(描画ロジックの重複を減らす)

📡 Turbo Streams(状態同期ロジックの重複を減らす)

🌿 Stimulus(必要最小の JS に限定)


🛠️ バリデーションは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
end

Turbo はフォーム送信後に 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 と Hotwire を比較するなら「速度」より「二重化の量」を見る

観点⚡ Hotwire(Single Logic)🧩 SPA(Dual Logic)
バリデーションRails に集約Rails + JS の二重化
エラー表示HTML 再描画JSON → UI マッピング
ルーティングTurbo DriveClient Router
状態同期Streams/partialStore/reconciliation
オフライン弱い(標準)強い(設計次第)
保守コスト低め高め

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

Advertisement
#turbo#rails#hotwire#frames#dual#logic#ロジック重複spa#dry#react#html#wire#バリデション
Advertisement