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

第8章 リンクとフォーム送信の仕組み

この章のねらい

第7章では、リンクのクリックが visit になることを見ました。この章では、フォーム送信の応答からページ表示を更新する流れことを理解します。

そして、フォーム送信ではコントローラが返す内容に約束ごとがあります。成功したときと失敗したときで、返すべきものが決まっているのです。この約束ごとは、第5部(Turbo Streams)と第7部(フォーム UX)の土台になります。第3部の中でもっとも大切な章です。

8.1 GET リンクと visit

第7章で見たとおり、リンクのクリックは visit になります。リンクは HTTP の GET リクエストです。Turbo Drive は GET の遷移を横取りし、body を差し替えます。

GET は、フォームでも使えます。たとえば検索フォームは GET です。検索ボックスに入力して送信すると、/tasks?q=... のような URL へ GET し、その結果で body が差し替わります。これも visit です(検索は第23章で実装します)。

GET は「サーバーの状態を変えない読み取り」です。次に見る、状態を変える送信とは扱いが分かれます。

8.2 POST / PATCH / DELETE の送信

タスクの作成・更新・削除は、サーバーの状態を変えます。これらは GET ではなく、POST(作成)・PATCH(更新)・DELETE(削除)で送ります。

従来の Rails では、フォームを送信するとページ全体が再読み込みされていました。Turbo Drive では、フォーム送信は専用の処理で扱われ、応答に応じて visit へ進むかエラー HTML を表示します。Turbo がフォーム送信を横取りし、バックグラウンドで送信し、返ってきた結果を画面に反映します。

ここで問題になるのが、「送信のあと、何を返すか」です。GET のリンクなら、行き先の HTML を返せば済みます。しかし、状態を変える送信では、成功する場合と失敗する場合があります。この 2 つで、返すべきものが変わります。

8.3 成功時は redirect

送信が成功したとき、コントローラは リダイレクトを返します。たとえばタスクを作成できたら、作成したタスクの詳細ページへリダイレクトします。

Relay の Task は scaffold で作ったので、create はすでにこの形になっています。

app/controllers/tasks_controller.rb(create の一部)

if @task.save
  redirect_to @task, notice: "Task was successfully created."
else
  render :new, status: :unprocessable_entity
end

成功時に redirect_to を返すと、Turbo はそのリダイレクト先へ visit します。結果として、作成したタスクの詳細ページへ画面が切り替わります。

更新(update)と削除(destroy)のリダイレクトには、もう 1 つ指定が付きます。

app/controllers/tasks_controller.rb(update と destroy の一部)

# update
redirect_to @task, notice: "Task was successfully updated.", status: :see_other

# destroy
redirect_to tasks_path, notice: "Task was successfully destroyed.", status: :see_other

status: :see_other は HTTP の 303 です。これは「この場所を GET で見に行ってください」という意味のリダイレクトです。

なぜ更新と削除にだけ付けるのでしょうか。PATCH や DELETE で送信したあと、Turbo はリダイレクトを追いかけます。このとき 303 を返すと、リダイレクト先を確実に GET で取得します。これがないと、リダイレクト先を元のメソッド(PATCH や DELETE)で取得しようとして、意図しない動きになることがあります。だから Rails の scaffold は、update と destroy のリダイレクトに status: :see_other を付けています。

8.4 失敗時は 422 でフォームを再描画する

送信が失敗したとき、たとえばタスクのタイトルが空でバリデーションに引っかかったときは、リダイレクトしません。入力中のフォームを、エラー付きで返します。

このとき大切なのが、HTTP ステータスです。8.3 のコードの失敗側を、もう一度見ます。

else
  render :new, status: :unprocessable_entity
end

render :new でフォームを描き直し、status: :unprocessable_entity(HTTP の 422)を付けています。

Turbo は、422 で返ってきたフォームの HTML で、いまの body を差し替えます。その結果、ページ遷移せずに、入力した内容とエラーメッセージがその場に表示されます。ユーザーは、入力をやり直せます。

なお、戻し先は操作によって変わります。作成(create)の失敗なら new を、更新(update)の失敗なら edit を、それぞれ 422 で再描画します。「失敗したら、いま編集していたフォームを 422 で返す」という形は同じです。

本書では、このステータスをコードで書くときは status: :unprocessable_entity を使います。HTTP のステータス名としては「422」と表記します。

8.5 status code は Turbo との契約

ここまでをまとめると、状態を変えるフォーム送信には、はっきりした約束ごとがあります。

  • 成功したら、リダイレクトを返す(更新・削除は 303)
  • 失敗したら、フォームを 422 で返す

この約束ごとを、本書では Turbo との契約と呼びます。Turbo は、フォーム送信の結果がリダイレクトなら visit し、422 ならその HTML で body を差し替える、という前提で動いているからです。

フォーム送信の契約。成功=redirect(update/destroy は 303)、失敗=422 render、200 は差し替わらず送信元 URL に留まる、という分岐を示すフローチャート

逆に、この契約を外すと正しく動きません。よくあるつまずきは、失敗時に 422 ではなく 200 でフォームを返してしまうことです。

Turbo Drive の非 GET フォーム送信に対して、リダイレクトなしの 200 HTML を返しても、Turbo はその HTML を画面に反映せず、送信元の URL にとどまります。理由は 2 つあります。1 つは、もし送信先(フォームの action)の URL に切り替えてしまうと、その後リロードしたとき、その action URL に対して GET が飛んでしまうからです。その URL は GET では存在しないかもしれません。もう 1 つは、POST のリロードでブラウザが出す「フォームを再送信しますか?」という確認を、Turbo が再現できないからです。

だから Turbo は、成功時はリダイレクト(更新・削除は 303)、エラー時は 4xx/5xx(通常は 422)で返すことを前提にしています。エラーを 200 で返すと、画面に反映されず、ユーザーにエラーが伝わりません。成功も失敗も、redirect か 422 に揃える、と覚えてください。

この契約は、第25章「バリデーションエラーとフォーム UX」で本格的に使います。ここでは「成功は redirect、失敗は 422」という対応を覚えておいてください。

8.6 この章の System Test

契約どおりに動いているかを、System Test で確かめます。Relay のタスク作成について、成功と失敗の 2 つを書きます。

第5章ではタスクをフラットな scaffold で作ったので、作成画面は new_task_path です。タスクには所属プロジェクトが必要なので、テストの中で先に用意します。

test/system/tasks_test.rb

require "application_system_test_case"

class TasksTest < ApplicationSystemTestCase
  setup do
    @project = Project.create!(name: "テスト用プロジェクト")
  end

  test "タスクを作成すると遷移して成功メッセージが出る" do
    visit new_task_path
    fill_in "タイトル", with: "最初のタスク"
    select @project.name, from: "プロジェクト"
    click_on "保存"

    assert_text "Task was successfully created"
  end

  test "タイトルが空だと同じ画面にエラーが出る" do
    visit new_task_path
    fill_in "タイトル", with: ""
    select @project.name, from: "プロジェクト"
    click_on "保存"

    assert_text "prohibited this task from being saved"
  end
end

1 つ目は成功のケースです。リダイレクトが起き、成功メッセージが表示されることを確認します。2 つ目は失敗のケースです。422 でフォームが差し替わり、エラー表示が出ることを確認します。ページ遷移していないことが、この契約の肝です。

項目とボタンは第5章で日本語へ揃えています。第16章以降は成功メッセージとエラー表示も変わるので、第28章の完成版テストへ置き換えます。

実行します。

bin/rails test:system

第8章では、フォーム送信の応答に応じて画面を更新し、成功は redirect、失敗は 422 という契約があることを見ました。次の 第9章 では、visit を速く見せるキャッシュと、Turbo 8 の morphing を扱います。

8.7 契約の適用範囲

200 HTML をそのまま成功応答にしない、という制約は Drive の非 GET 送信についてです。Frame は対応する frame を含む 200 HTML を描画でき、200 の Turbo Stream も処理されます。失敗を成功と区別するため、どの経路でもバリデーション失敗には 422 を返します。作成時も 303 を明示すると意図が揃います。

System Test のエラー文表示だけでは HTTP ステータスを検証できません。リクエストテストでは認証後に無効な入力を POST し、assert_response :unprocessable_entity を使います。

参考資料