Using ViewComponents with ActionCable 
Tags: rails, actioncable, viewcomponent
Published: 2020-02-25 14:27:14

---
title: Using ViewComponents with ActionCable 
published: true
description: An overview of view components in Rails
tags: rails, actioncable, viewcomponent
---

I've had a lot of fun lately using ViewComponents (known as ActionView::Component but will be [renamed soon](https://github.com/github/actionview-component/issues/206)).

It is a framework, from the GitHub team, that allows you to build "components" that can be rendered. They sit in the `/app/components` directory, and each component has a class and a view.

{% github github/actionview-component no-readme %}

To create a component, you can use the included generator. For example, let's create a component for an `Item` that has a title and a url. Our component will simply require an item.

``` shell
$ bin/rails g component Item item
```

These files will be created:

- `app/components/item_component.rb`
- `app/components/item_component.html.erb`
- `test/components/item_component_test.rb`

Unlike using partials and decorators (concerns, helpers, etc), components can be tested, and must be since we can reuse the same component in different views (and even with ActionCable as we'll see later).

This is how our `item_component_test.rb` looks like:

``` ruby
require "test_helper"

class ItemComponentTest < ActionView::Component::TestCase
  test "renders the item title" do
    item = Item.new(title: 'Sample item', url: 'https://dev.to/')

    assert_match(
      item.title,
      render_inline(ItemComponent.new(item: item)).to_html
    )
  end
end
```

We're simply expecting to find the title of the `Item` in the rendered component.

We can now define the component, which receives an `Item` in its constructor.

``` ruby
class ItemComponent < ActionView::Component::Base
  validates :item, presence: true

  def initialize(item:)
    @item = item
  end
  attr_reader :item
end
```

Note how we can validate the presence of an `Item`.

Our view, for sake of simplicity, will just render the item's title and URL as a link wrapped in a div with an id.

``` erb
<div id="item-<%= item.id %>">
  <%= link_to item.title, item.url, target: '_blank' %>
</div>
```

Our component is a class, meaning we can take advantage of creating the methods we need. In this case, we can move the id generation to a public method and, that way, we can later access it from other places.

With this new method, the `ItemComponent` looks like this now:

``` ruby
class ItemComponent < ActionView::Component::Base
  validates :item, presence: true

  def initialize(item:)
    @item = item
  end
  attr_reader :item
  
  def dom_id
    "item--#{item.id}"
  end
end
```

And we updated the view:

```erb
<div id="<%= dom_id %>">
  <%= link_to item.title, item.url, target: '_blank' %>
</div>
```

In the test, we should also check for that id is present in the `dom_id` method to ensure uniqueness.

``` ruby
require "test_helper"

class ItemComponentTest < ActionView::Component::TestCase
  test "renders the item title" do
    item = Item.new(title: 'Sample item', url: 'https://dev.to/')

    assert_match(
      item.title,
      render_inline(ItemComponent.new(item: item)).to_html
    )
  end

  test "#dom_id" do
    item = Item.create(title: 'Sample item', url: 'https://dev.to/')

    assert_match(
      item.id,
      ItemComponent.new(item: item).dom_id
    )
  end
end
```

That's it! The component is ready and all tests pass.

We can now render this component in our views:

``` erb
<% @items.each do |item| %>
  <%= render ItemComponent.new item: @item %>
<% end %>
```

### Integrating with ActionCable

ViewComponents are a great companion to ActionCable. As DHH said, we can ditch SPA and simply send HTML over the wire, and doing that is straightforward with ViewComponents.

{% twitter 1223251586874396672 %}

In our example, the `Item` belongs to a `Board`. When a user updates an item, we want to show the updates to other users seeing the same board.

We've already setup ActionCable and we have a `BoardChannel` that users connect to. We can broadcast the item update to this channel from the `ItemsController`:

``` ruby
class ItemsController < ApplicationController
  before_action :set_board

  def update
    if @item.update(item_params)
      item_component = ItemComponent.new(item: @item)
      BoardChannel.broadcast_to(
        @board, event: 'item_updated', 
        payload: {
          id: @board.id,
          item_id: @item.id, 
          dom_id: item_component.dom_id, 
          html: view_context.render(item_component) 
        }
      )
      
      redirect_to @board
    else
      render :edit
    end
  end

  # ...
end
```

As you can see, the `ItemComponent` is rendered and sent part of the payload of the broadcast. We're also sending the `dom_id`, which we can use in javascript to find the DOM element and update its HTML with the one provided.

Here's our StimulusJS controller, with the connection to the `BoardChannel` and a function to process the events received:

``` js
import { Controller } from 'stimulus'
import consumer from '../channels/consumer';

export default class extends Controller {
  static targets = ['items']

  connect() {
    const that = this;

    consumer.subscriptions.create({
      channel: 'BoardChannel',
      id: that.context.element.dataset.id
    }, { received(data) {
      that.receivedEvent(data);
    } })
  }

  receivedEvent(data) {
    switch (data.event) {
      case 'item_updated':
        const element = document.getElementById(data.payload.dom_id);
        if (element) {
          element.outerHTML = data.payload.html;
        }
        break;
    }
  }
}

```

Our views will now behave in real-time! 

In the future, when we make changes to the `ItemComponent`, we won't have to worry about updating the javascript or any ActionCable logic. It will simply work.

### Final words

I've been using ViewComponents in all personal projects and, recently, at work. 

The encapsulation, testability, and performance it provides are simply amazing. I look forward to the moment ViewComponents is made part of Rails core.