17
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Qiita株式会社Advent Calendar 2021

Day 20

Rails 6.1 の Renderable objects でお手軽に脱 ERB する

Last updated at Posted at 2021-12-19

こんにちは。 Qiita Team の開発に参画している @tomoasleep です。
巷では、Rails 7 で盛り上がってますが、今回は Rails 6.1 で導入された Renderable objects (正式名が無いのですが開発中の名称からこの記事ではこう呼びます) についての紹介していきたいと思います。

React 側に View のロジックが寄っていて、あまり Rails の View にロジックをもたせていなかったケースなどでは、これを用いて汎用的な View を用意することで、ERB や Slim での View 実装を減らせる可能性があります。そういった例も紹介していこうと思います。

Rails 6.1 で render#render_in を実装したオブジェクトを渡せるようになった

Rails 6.1 では以下のように、View や Controller の render メソッドに render_in を実装したオブジェクト (Renderable object) を渡すことができるようになりました。

<h1>Header</h1>
<%= render HelloComponent.new %>
class HelloComponent
  def render_in(view_context)
    # view_context を介して helper method などの view で使える method を呼び出せる
    view_context.content_tag(:p, "Hello, World!")
  end
end

この場合、 render_in メソッドの返り値が描画され、以下のような表示になります。

image.png

Controller の render にも渡すことができる (※ format メソッドの実装も必要)

Renderable object は View 内の render だけでなく、 Controller 内の render にも渡すことができます。

class HelloComponentController
  def index
    render SampleComponent.new
  end
end

ただし、Controller の render メソッドに渡すオブジェクトには format メソッドも併せて実装する必要があります。(実装されていないと、以下のようなエラーとなります)

image.png

class HelloComponentComponent
  def render_in(view_context)
    view_context.content_tag(:p, "Hello, World!")
  end

  # Response の Content-Type を何にするかを symbol で返す
  def format
    :html
  end
end

また、 Renderable object を渡した場合も、Layout は使用されます。なので、以下の通りに、 content_for と併用して、タイトルなどを Renderable object から制御することも可能になっています。

class HelloComponentComponent
  def render_in(view_context)
    view_context.content_for(:title, "Title from Renderable objects")

    view_context.content_tag(:p, "Hello, World!")
  end

  def format
    :html
  end
end
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "RenderableExample" %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
  </head>

  <body>
    <%= yield %>
  </body>
</html>

image.png

これにより、 Action の View に ERB を書かない形で置き換えることが可能です。

(元々は ViewComponent を意識して導入された機能だった)

この機能は、Introduce support for 3rd-party component frameworks by joelhawksley · Pull Request #36388 · rails/rails で導入されました。

この機能は GitHub が開発した、 ViewComponent という (class ベースで) View を構築するためのフレームワークを想定して実装されました。
この機能で、ViewComponent が、 render メソッドをオーバーライドせずとも組み込めるようにすることを意図されていました。

が、実は ViewComponent を必ずしも利用しなくても、 render_in というメソッドを実装しているオブジェクトを用意すれば渡せる、という仕様になっています。

応用例: React に描画を任せているような View を Renderable object に置き換えられる

実際に、これを応用する例として、例えば、 View の描画の大部分を React などに任せていて、Rails の View が持つ責務が小さい例などが挙げられます。

例えば、shakacode/react_on_rails を利用している例だと、以下のような ERB を書くことになります。

<% content_for(:title, 'Hello World') %>
<%= react_component("HelloWorld", props: { name: @name }) %>
class HelloWorldController < ApplicationController
  def index
    @name = 'Stranger'
  end
end
const HelloWorld = ({ name }) => {
  return (
    <div>
      <h3>Hello, {name}!</h3>
    </div>
  )
}

これを、Renderable object に置き換えると以下のような実装になり、Action 毎の View を利用しない形に置き換えることができるようになります。

class ReactPage
  def initialize(component_name, title:, props: {})
    @component_name = component_name
    @title = title
    @props = props
  end

  def render_in(view_context)
    view_context.content_for(:title, @title)
    view_context.react_component(@component_name, props: @props)
  end

  def format
    :html
  end
end
class HelloWorldController < ApplicationController
  def index
    render ReactPage.new('HelloWorld', title: 'Hello World', props: { name: 'Stranger' })
  end
end

Renderable object で置き換えることで、以下のメリットが考えられます。

  • View にわたすパラメータのチェックが行いやすい、値の依存関係の追跡がしやすい
    • コンストラクタを介した値の受け渡しを行うので、インスタンス変数、locals を用いる場合よりも、意図しないパラメータでエラーを出したりなどのチェックがしやすかったり、どこから渡されているかを明確にしやすいです。
    • 入力値のチェック、加工などの実装も ERB などよりも Ruby の class としてのほうが書きやすいことが多いです。
  • ActionView ではなく React 側に View の実装を寄せていきたい場合、それを強制させやすくなる

Action 毎の View を書かなくて良いメリットは、持たせるロジックが小さい場合に有効に働きやすいです。

また、パラメータの追跡、チェックのしやすさなどは、View の構造化をする上で有効に働きやすく、partial だとやりにくいところだと思うので、 View をまるごと置き換えるだけでなく、一部の partial を置き換えるというやり方でも有用だったりします。

(view_context のメソッドを呼び出すことで、頑張れば様々な View を構築できる)

一応 render_in に渡される view_context から View helper のメソッドは呼び出せるので、一応 template で出来ることは Renderable でも出来ます。

…が、ある程度複雑な View を構築したくなったら ViewComponent を使うのを検討したほうが良いと思います。

ViewComponent は、Template Engine のサポートや i18n サポート、テスト用のヘルパーなど、View を実装する際に役立つ様々な機能を持っているので、既存の ERB でガリガリ書かれている View や partial を置き換えたりする用途では、こちらがおすすめです。

おわりに

Renderable objects を利用して、簡単な View を ERB などを利用しない形で置き換えられる例を紹介しました。高度な機能を利用しなければ ViewComponent などのライブラリ無しでも十分なので、試しに使ってみるのも良いかもしれません。

明日の Qiita株式会社 Advent Calendar 2021@phigasui が担当します! :christmas_tree:

17
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?