はじめに
Railsで開発中、フォームの実装時にrenderの扱いについて悩むことがあったため、調べてく内にredirect_toとrenderの違いを改めて再確認出来たので、備忘として残しておくものです。
ついでにrenderで一瞬悩んだことも書いておきます。(具体的にはbefore_actionの扱い)
環境
・Ruby 2.6.5
・Rails 6.0.3
状況
飲食店情報を登録する様なアプリケーションを作っており、コントローラーに以下の様なアクションを設定していました。
def new
@shop = Shop.new
end
def create
@shop = @owner.shops.new(shop_params)
if @shop.save
redirect_to :index
else
render :new
end
end
*ストロングパラメータは今回関係ないので割愛
もはや何も考えずこう書いていましたが、保存に失敗した場合、renderを用いてもう一度投稿フォームに戻すのは何故か?という所を振り返ってみます。
結論としては、入力した内容を保持したままもう一度入力フォームに戻したいからです。
では何故その場合redirect_toではなくrenderを使えば実現できるのでしょうか?
redirect_toとrenderの挙動について振り返ってみます。
redirect_toとは
redircet_toは指定したアクション(やパス)へのアクセスをクライアントサイドに行わせる処理です。
これにより、MVCの流れを再度踏まえた上でそのページにアクセスすることになります。
具体的には「指定されたアクションに再度アクセス⇨コントローラーのアクションを実行⇨対応するビューを表示」という流れですね。
アクションのビューにurlからアクセスする際の挙動と全く同じことをやってくれるわけです。
renderとは
それに対しrenderは、アクションを再度経由することなく指定したアクションに対応するビューをただ表示してくれます。
redirect_toと違い、newアクションの中身を実行することなくビューを返してくれるわけです。
renderで入力内容が保持される訳
先程の説明を踏まえてもう一度コードを見てみましょう。
def new
@shop = Shop.new
end
def create
@shop = @owner.shops.new(shop_params)
if @shop.save
redirect_to :index
else
render :new
end
end
newアクションはお店情報の新規登録ページですので、アクション内で@shop = Shop.newを定義しています。(空のインスタンスを生成)
この後フォームに入力した値がparamsとして渡され、それがcreateアクションで今回作成した@shopの情報としてうまく渡されることで保存ができるわけです。
保存に失敗した場合(elseの場合)またフォームに戻す処理をする訳ですが、この時にredirect_toを使うと、再度newアクションが実行され@shopはまた空のインスタンスになってしまいます。
一方でrenderを使うとnewアクションを介さないため、@shopにはcreateアクションで受け取ったparamsの値が保持されたまま、再度フォームのビューを表示してくれる訳です。
.shop-wrapper
= form_with model: [@owner, @shop], html: {class: "shopform"}, local: true do |f|
上記はビューの抜粋ですが、フォームは@ownerと@shopを受け取って内容を表示しているため、@shopに中身がある状態であればその中身をちゃんと表示してくれる訳です。
以上がcreateアクションに失敗した時に、renderを使うべき理由になります。
今回遭遇したエラー
ちなみに今回はrenderを設定した際、最初以下のエラーに遭遇しました。
一瞬なんだこれってなったんですが、newアクションに対して以下のbefore_acitonを設定してたんですよね。
before_action :set_select_lists, only: [:new]
# 中略
private
def set_select_lists
@stations = Station.all.map {|station| [station.name, station.id] }.unshift(["以下から選んでください", nil])
@genres = Genre.all.map {|genre| [genre.name, genre.id] }.unshift(["以下から選んでください(必須)", nil])
@marks = Mark.all.map {|mark| [mark.favorite, mark.id] }.unshift(["以下から選んでください(必須)", nil])
end
フォームの中にあるプルダウンの選択リストを作り、before_actionで設定していたんです。
先ほども書いた様に、renderはnewアクションを再度経由しないため、before_actionも行わない→設定された値がなくビュー表示でエラーが起きるという流れだったわけです。
これを受け一瞬、「@shop意外に定義しなきゃいけない変数があったらrender使えないの?」って思ったんですが、数分考えてすぐ解決しました。
createアクションを以下の様に書き換えます。
def create
@shop = @owner.shops.new(shop_params)
if @shop.save
else
set_select_lists
render :new
end
end
createアクションでrenderする前に、before_actionで行っていたset_select_listsを行わせてあげることで、その値も取得した状態でrenderすることが出来ます(renderはあくまでcreateアクションを行った上でnewのビューを表示しているため)
一瞬???となったんですがそんな複雑な話でもなかったですね。
終わりに
実はredirect_toとrenderの挙動は、Progateで初めてプログラミングに触れ1ヶ月程度で一番悩みぬいた所だったんですよね。ここの違いに気づいた時にMVCへの理解も同時に深まったというイメージがありました。
僕と同じ様な初学者の方に何かの参考になればと思います。
*初学者ゆえ、何かあればご指摘いただけると大変ありがたいです。