Hotwire @ Skroutz: Lazy load data with minimum effort

At Skroutz we constantly try to find ways to make our website faster and consequently optimize our users’ experience. In this context, Hotwire couldn’t escape our attention as it aroused the interest of the developers community from the moment it was announced by its creators.

What is Hotwire?

Hotwire, as described in the official website, is

an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire

In other words, Hotwire creates HTML markup, instead of JSON objects, and sends it as response to a request from the client. This way, we avoid the manipulation of the response data with Javascript.

Furthermore, Hotwire can find the way to automatically inject the received HTML into the right place of the DOM, with Turbo, a set of techniques that eliminate the need of writing custom Javascript in order to handle form submissions, partial DOM updates, history changes and many more.

As stated in their documentation, Turbo is able to handle at least 80% of the cases by itself on the client side, without the need for any Javascript to be written by you. For the remaining 20% of the cases, Hotwire provides Stimulus, a lightweight Javascript framework that works well with Turbo. Stimulus can be used to create reusable components that can be bound to any HTML element and enhance it with custom behaviour.

The order show page

Let’s get started by setting the context of our example. At Skroutz we have developed a portal, known as Skroutz Merchants, that provides useful tools to our partners in order to facilitate the operation of their store. In one of those views we show the order’s details alongside a list of tickets that may exist and are related to this order.

In order to reduce the initial rendering time, we choose to load the tickets list asynchronously, as soon as the initial render has finished.

The following image illustrates a simplified wireframe of the order show page. The parts that are loaded on the initial render, such as the sidebar, the top bar and the order itself, are colored with green. The tickets list section is colored in orange, indicating that it gets loaded asynchronously, after the initial render.

image
Image 1: Merchants panel: Order show

Lazy load with vanilla Javascript

The process is simple: as soon as the page loads, a javascript function makes a request to /merchants/orders/:code/tickets path in order to fetch the tickets, if any.

As shown in the following block, order_tickets queries the database, checks to see if there are any tickets and creates the HTML from the respective partial template.

# orders_controller.rb

# GET /merchants/orders/:code/tickets
def order_tickets
  tickets = # db query

  options = { layout: false, formats: :html }
  view = if tickets.present?
            options.merge!(partial: 'merchants/tickets/ticket',
                          collection: tickets,
                          as: :ticket)
          else
            options.merge!(partial: 'merchants/tickets/no_tickets_message')
          end

  respond_to do |format|
    format.json do
      render json: { html: render_to_string(view).squish }, status: :ok
    end
  end
end

Now, let’s see the frontend part. OrderTicketsView is the class that is responsible for fetching the tickets data and injects the received markup into the DOM. More specifically, _getOrderTicketsData performs the asynchronous request, finds the #js-tickets-wrapper element and replaces it with the received markup.

<%# show.html.erb %>
...
<div id="js-tickets-wrapper" data-order-code="<%= order.code %>">
  <div class="loading-tickets flex-row">
    <%= render 'merchants/shared/spinner' %>
  </div>
</div>
...
// order_tickets_view.js

export default class OrderTicketsView {
  constructor() {
    this._cacheElements();
    this._getOrderTicketsData();
  }

  _cacheElements() {
    this._ticketsWrapper = document.getElementById('js-tickets-wrapper');
    if (this._ticketsWrapper) {
      this._orderCode = this._ticketsWrapper.dataset.orderCode;
    }
  }

  _getOrderTicketsData() {
    if (this._orderCode) {
      const orderTicketsUrl = `${this._orderCode}/tickets`;

      axios.get(orderTicketsUrl)
        .then(({ data }) => {
          this._appendTicketsGrid(data.html);
        })
        .catch(() => {
          this._showErrorMessage();
        });
    }
  }

  _appendTicketsGrid(tickets) {
    this._ticketsWrapper.parentElement.innerHTML = tickets;
  }

  _showErrorMessage() {
    this._ticketsWrapper.innerHTML = `<div class="box-alert error">${__(
      'Failed loading tickets'
    )}</div>`;
  }
}

As we can see from order_tickets_view.js file, we have to write a fair amount of custom javascript code to achieve a lazy loading behaviour. Wouldn’t it be nice if we had a way to apply this lazy loading feature without the boilerplate javascript code?

Introducing Turbo Frames

Fortunately, Turbo provides Turbo Frames, a set of techniques that help us decompose a page into independent parts that get updated individually.

Turbo frame is nothing more than a custom HTML element with the <turbo-frame> tag. Every turbo frame element must have a unique id that is used by Turbo in order to update its contents. Anything that is wrapped within a <turbo-frame> tag, belongs to a separate context that gets updated independently of the rest of the page.

Lazily loading frames is a special case of turbo frames that fits perfectly to our case. In order to create a lazily loading frame we just have to provide a src attribute to the <turbo-frame> element with a url as the value. As soon as the <turbo-frame> element gets rendered, Turbo will make a request to the provided url and try to update the frame’s contents with the received HTML (As we said earlier, Hotwire responds with HTML instead of JSON). This update happens automatically by Turbo and we don’t have to write any custom javascript to handle the response.

Applying lazily loading frames

Introducing turbo frames to an existing codebase is quite simple. Just wrap the desired part of the page with a <turbo-frame> tag and you have created a frame.

In this way, in show.html.erb view, we replace the #js-tickets-wrapper div with a <turbo-frame> tag. The new turbo frame element must have a unique id, so we assign the order_tickets id, alongside with a url as value of the src attribute. Finally, we add the loading: 'lazy' attribute so that the request to the provided url happens only when the turbo frame element becomes visible in the viewport. More details about the available HTML attributes can be found here.

<%# show.html.erb %>
...
<%# <div id="js-tickets-wrapper" data-order-code="<%= order.code %>"> %>
<%= turbo_frame_tag :order_tickets,
                    src: tickets_merchants_order_path(code: @order.code),
                    loading: 'lazy' do %>
  <div class="loading-tickets flex-row">
    <%= render 'merchants/shared/spinner' %>
  </div>
<% end %>
<%# </div> %>

Then, we have to adjust the response of the action that gets called when the turbo frame element requests the provided url. Turbo frame waits for a response that contains HTML markup, so we alter the contents of the respond_to block in order to return the respective partial view. Furthermore, we no longer need the options and the view objects because we don’t build the HTML manually, as we did before with render_to_string.

# orders_controller.rb

# GET /merchants/orders/:code/tickets
def order_tickets
  tickets = # db query

  # options = { layout: false, formats: :html }
  # view = if tickets.present?
  #           options.merge!(partial: 'merchants/tickets/ticket',
  #                         collection: tickets,
  #                         as: :ticket)
  #         else
  #           options.merge!(partial: 'merchants/tickets/no_tickets_message')
  #         end

  respond_to do |format|
    # format.json do
    #   render json: { html: render_to_string(view).squish }, status: :ok
    # end
    format.html do
      if tickets.present?
        render partial: 'merchants/tickets/tickets', locals: { tickets: tickets }
      else
        render partial: 'merchants/tickets/no_tickets_message'
      end
    end
  end
end

There is something more that we need to do. We’ll have to adjust the merchants/tickets/no_tickets_message partial so that it responds with the expected markup. merchants/tickets/tickets has been created from the beginning in order to wrap the collection of tickets in a way that Turbo can handle.

Turbo frame has to find a way to match the content it receives from the request to the provided url, with the part of the page that it needs to update. As we said earlier, we gave the order_tickets id to the turbo frame element. Turbo will try to find a <turbo-frame> tag with the same id inside the response body, and if it finds it, it takes its contents and replace the contents of the #order_tickets turbo frame element of the page with them.

So, nothing scary, just wrap the contents with a <turbo-frame> tag with the appropriate id as shown in the following blocks.

<%# _tickets.html.erb %>

<%= turbo_frame_tag :order_tickets do %>
  <%= render partial: 'merchants/tickets/ticket',
             collection: tickets,
             as: :ticket %>
<% end %>
<%# _no_tickets_message.html.erb %>

<%# Previously %>
<%# <div class="box-alert warning"> %>
<%#   <%= _('No tickets found') %>
<%# </div> %>

<%# Add turbo_frame_tag %>
<%= turbo_frame_tag :order_tickets do %>
  <div class="box-alert warning">
    <%= _('No tickets found') %>
  </div>
<% end %>

Oh, and don’t forget, we no longer need the custom javascript code from order_tickets_view.js, so, we can safely delete it!

And that’s it! In three simple steps we have introduced Turbo Frames to our codebase in order to achieve the same lazily loading behaviour, without the use of custom javascript.

Summary

In this post, we tried to demonstrate the ease with which we can use Turbo Frames. We have completed the refactoring in three simple steps:

  • Wrap the desired part of the page with a <turbo-frame> tag and give it a unique id and a url as the value of the src attribute
  • Refactor the controller’s response as needed
  • Add the <turbo-frame> tag with the appropriate id to the partials that get rendered from the controller

Except from the simplicity of this refactoring, we have managed a small shrinkage of our codebase (as shown in the following image from Github), as a result of the removal of unwanted custom javascript code that was used to handle these updates that now, automatically, get handled by Turbo.

image
Image 2: Github: Lines removed and added

Next steps

Turbo comes with many more techniques, apart from Turbo Frames. Turbo Streams is another powerful feature that can improve the dynamic nature of any app. We can use streams to broadcast changes to our models, from the server to the client. And this is done with a WebSocket connection that Turbo, automatically establishes and handles for us.

In our case, we can take advantage of the power of Turbo Streams and push any updates of a specific order’s tickets to the client, so, users will be able to see a live update (insertion of a new ticket, deletion or edit) on their screen, without having to constantly refresh the page to fetch the latest state.