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 になることを見ました。この章では、フォーム送信も同じ visit になることを理解します。

そして、フォーム送信では controller が返す内容に約束ごとがあります。成功したときと失敗したときで、返すべきものが決まっているのです。この約束ごとは、第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 として扱われます。Turbo がフォーム送信を横取りし、バックグラウンドで送信し、返ってきた結果を画面に反映します。

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

8.3 成功時は redirect

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

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

app/controllers/tasks_controller.rbcreate の一部)

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.rbupdatedestroy の一部)

# 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 は、updatedestroy のリダイレクトに 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 でフォームを返してしまうことです。

状態を変えるフォーム送信に対して 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 "Title", with: "最初のタスク"
    fill_in "Project", with: @project.id
    click_on "Create Task"

    assert_text "Task was successfully created"
  end

  test "タイトルが空だと同じ画面にエラーが出る" do
    visit new_task_path
    fill_in "Title", with: ""
    fill_in "Project", with: @project.id
    click_on "Create Task"

    assert_text "prohibited this task from being saved"
  end
end

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

フォームの項目名(TitleProject など)や成功・エラーの文言は、scaffold が生成したビューに由来します。生成された画面に合わせて読み替えてください。Project という項目名は、project_id のラベルを humanize したものです(project_idProject)。所属プロジェクトを指定するため、ここではプロジェクトの ID を入力しています。

実行します。

bin/rails test:system

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

参考資料