Making Hotwire play nice with ViewComponent
Tags: rails
Published: 2021-05-10 13:04:59

---
title: Making Hotwire play nice with ViewComponent
published: true
date: 2021-05-10 12:51:04 UTC
tags: RubyonRails
canonical_url: https://blog.kuda.dev/making-hotwire-play-nice-with-viewcomponent-ckoilssb40msdv9s10v139kgs
---

Have you ever wondered if you could use [hotwire](hotwire.dev) with [`view_component`](https://github.com/github/view_component) instead of partials. If you have, maybe tried and failed, welcome here.

By default [hotwire](https://hotwire.dev) is made to work with partials, which is the built in way to extract 'components' in the context of rails based apps. In a certain application I was building I have used [`view_component` (inspired by react)](https://github.com/github/view_component) in place of partials because it is a framework for building reusable, testable & encapsulated view components in Ruby on Rails.

It was all fun and games until I wanted to make [hotwire](https://hotwire.dev) to work with the view components I had made.

We will use an e-commerce application as an example to demonstrate the problem at hand.

![freshstore-gif.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1620650508471/XwXipgBgf.gif)

_NB: no JavaScript was used in making this interactive, except for displaying the loading spinners_

Hotwire uses websockets to broadcast changes to all clients listening, and this is very fast considering websockets maintain a persistent open connection with the client and the changes feel almost instantaneous. In this example when a user adds a product to cart, [hotwire](https://hotwire.dev) will broadcast the new product component and it will be replaced on the frontend, smooth as an SPA, with minimum javascript.

When a product is added to cart, we want to show it change on the UI, unlike a traditional API request which returns JSON, hotwire returns html, in the form of turbo streams. When the response arrives it will only change the specific section in the page that has a turbo frame that matches a specific id.

Using view components in this context would also help reduce the response's payload to only contain that section that has to be replaced. However view components and turbo streams dont play nice together so I googled around for a quick solution and found [this answer](https://discuss.hotwire.dev/t/viewcomponents-not-rendering-with-rails-stream-helper/1813/2) on the hotwire forum. I then used that answer to successfully render a ViewComponent in the response.

```
# app/controller/carts_controller.rb

class CartsController < ApplicationController
  def update
    product = Product.find(cart_params[:product_id])
    line_item = CartUpdateService.call(@product, cart_params[:quantity])
    product_component = ProductComponent.new(
      product: product,
      quantity_in_cart: @line_item.quantity
    )

    respond_to do |format|
      format.turbo_stream {
        stream = turbo_stream.replace product do
           view_context.render(product_component)
        end

        render turbo_stream: stream

        head :ok
      }
    end
end

```

This was a good solution until I realised that you can only render a stream once, so this crumbled when I wanted to broadcast changes to other areas of the application, like adding `line_items` to the cart in the UI, updating cart count, etc. This is because, by design turbo broadcasts are meant to be initiated in the model, which I sort of thought as a code smell because not only was the model responsible for data persistence, validations and business logic, it was also responsible for the way the UI worked. We all strive for [skinny models](https://thoughtbot.com/blog/skinny-controllers-skinny-models), don't we.

This was how it was before, when using partials

```
# app/models/line_item.rb
class LineItem < ApplicationRecord
  ...

  after_create_commit :broadcast_prepend_line_item, if: :current_user_present?

  private def broadcast_prepend_line_item
    broadcast_prepend_to "#{Current.user.id}:line_items", partial: 'line_items/line_item', locals: { line_item: self, store: self.order.store }
  end
  ...
end

```

And as far as I had researched there was no way to use View Component instead of partials to broadcast these updates.

So I went and dug into the [hotwire source code](https://github.com/hotwired/turbo-rails) to look for clues whether it was possible or not.

After series of iterations I came up with the concept of `LiveComponent`, a class that would inherit from `ViewComponent::Base` but also include hotwire modules to allow broadcasts and streams. Here is the complete code

```
# app/components/live_component.rb

class LiveComponent < ViewComponent::Base
  include Turbo::FramesHelper, Turbo::Streams::StreamName, Turbo::Streams::Broadcasts

  attr_reader :streamable, :target

  def initialize(view_context: nil, **args)
    @view_context = view_context
  end

  def broadcast_replace
    return unless @view_context.present?

    broadcast_replace_later_to(
      streamable,
      target: target,
      content: @view_context.render(self)
    )
  end

  def broadcast_prepend
    return unless @view_context.present?

    broadcast_prepend_to(
      streamable,
      target: broadcast_target_default,
      content: @view_context.render(self)
    )
  end

  def broadcast_remove
    return unless @view_context.present?

    broadcast_remove_to(
      streamable,
      target: target
    )
  end

  private def broadcast_target_default
    target.class.model_name.plural
  end
end

```

And this is exactly a ViewComponent with some live extras baked in. Continuing with out illustration of broadcasting changes to the line\_items in the cart, we would implement it as a live component like

```
# app/components/live_line_item_component.rb

class LiveLineItemComponent < LiveComponent
  def initialize(view_context: nil, line_item:, current_user:)
    @line_item = line_item

    # these will be used by LiveComponent to identify
    # the stream channel and the targeted frame-tag on the UI
    @streamable = "#{current_user.id}:line_items"
    @target = @line_item

    super
  end

  def render?
    @line_item.quantity > 0
  end
end

```

and in the html templates you can then define

```
<%= turbo_frame_tag dom_id(@line_item) do %>
   <!-- line item html logic -->
<% end %>

```

Now in `cart_controller.rb` when you add to cart you can then use the live component

```
    respond_to do |format|
      format.turbo_stream {
        live_line_item_component = LiveLineItemComponent.new(view_context: view_context, line_item: @line_item, current_user: Current.user)
        if @line_item.destroyed?
          live_line_item_component.broadcast_remove
        elsif @line_item.quantity == 1 && @line_item.updated_at == @line_item.created_at
          live_line_item_component.broadcast_prepend
        else
          live_line_item_component.broadcast_replace
        end

        ...
    }

```

I know the code is a little bit 'dirty' and I'm sure some of you smart people will find ways to improve on it.

It was a fun little exploration and I am happy it worked out.

Cheers 🥂