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

こんにちは。 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) を渡すことができるようになりました。

```erb
<h1>Header</h1>
<%= render HelloComponent.new %>
```

```ruby
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/e7899264-2c44-f98b-d8d7-324cb04fbd2f.png)


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

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

```ruby
class HelloComponentController
  def index
    render SampleComponent.new
  end
end
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/1aea6577-e9cb-94af-918f-0c92cf96f0ac.png)


```ruby
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 から制御することも可能になっています。

```ruby
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
```

```erb
<!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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7048/d874d710-d8db-1237-2205-7c7230763280.png)

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

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

この機能は、[Introduce support for 3rd-party component frameworks by joelhawksley · Pull Request #36388 · rails/rails](https://github.com/rails/rails/pull/36388) で導入されました。

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

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


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

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

例えば、[shakacode/react_on_rails](https://github.com/shakacode/react_on_rails) を利用している例だと、以下のような ERB を書くことになります。

```erb
<% content_for(:title, 'Hello World') %>
<%= react_component("HelloWorld", props: { name: @name }) %>
```

```ruby
class HelloWorldController < ApplicationController
  def index
    @name = 'Stranger'
  end
end
```

```jsx
const HelloWorld = ({ name }) => {
  return (
    <div>
      <h3>Hello, {name}!</h3>
    </div>
  )
}
```

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

```ruby
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
```

```ruby
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](https://viewcomponent.org/) を使うのを検討したほうが良いと思います。

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

## おわりに

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

明日の [Qiita株式会社 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/qiita) は @phigasui が担当します！ :christmas_tree: