Take advantage of Turbo Streams in event handlers

… and check why 5600+ Rails engineers read also this

Take advantage of Turbo Streams in event handlers

It’s been a time since Rails 7 came with Turbo and its Turbo Streams.

At first, I was skeptical because the idea of broadcasting view updates as a sort of Active Record callbacks. Sorry, I’m simply not buying the idea of mixing WebSockets calls into a data model. However, rejecting the concept of Turbo::Broadcastable concern, I see Turbo Stream as a great tool, and I’m sure there is a proper place for it in the Rails app architecture.

This is more or less what our typical architecture looks like. Read models are loaded and presented on the UI. A user issues a command which is passed to the domain layer. This usually culminates in one or more domain events being published. These events are persisted and then handled synchronously or asynchronously by event handlers which update the read models. With the next page load, the user sees the updated read models. The circle is closed.

With Turbo Streams and just one more event handler, we can invoke asynchronous direct updates from the backend to the UI and significantly improve user experience.

Let’s see how we do it based on the ecommerce, our demo application.

<table>
  <thead>
    <tr>
      <td>Number</td>
      <td>Customer</td>
      <td>State</td>
    </tr>
  </thead>

  <tbody>
  <% @orders.each do |order| %>
    <%= turbo_stream_from "orders_order_#{order.uid}" %>
    <tr>
      <td><%= order.number %></td>
      <td><%= order.customer %></td>
      <td id="<%= "orders_order_#{order.uid}_state" %>"><%= order.state %></td>
    </tr>
  <% end %>
  </tbody>
</table>

class Configuration
  def call(event_store)
    @event_store = event_store

    # ... handlers syncing Active Record models omitted

    subscribe(
      ->(event) { broadcast_order_state_change(event.data.fetch(:order_id), 'Submitted') },
      [Ordering::OrderSubmitted]
    )
    subscribe(
      ->(event) { broadcast_order_state_change(event.data.fetch(:order_id), "Paid") },
      [Ordering::OrderConfirmed]
    )
    subscribe(
      ->(event) { broadcast_order_state_change(event.data.fetch(:order_id), "Cancelled") },
      [Ordering::OrderCancelled]
    )
  end

  private

  def subscribe(handler, events)
    @event_store.subscribe(handler, to: events)
  end

  def broadcast_order_state_change(order_id, new_state)
    Turbo::StreamsChannel.broadcast_update_later_to(
      "orders_order_#{order_id}",
      target: "orders_order_#{order_id}_state",
      html: new_state)
  end
end

Boom! Any time we catch an OrderSubmitted, OrderConfirmed, or OrderCancelled event, we invoke broadcasting an update. Every subscribed client receives a Turbo Streams message and updates the specific order state. Page reload is not required.

You might also like