Using StimulusReflex to Compose your UI Dynamically

Imagine giving your users the power to construct their UI, piece by piece, like Lego blocks. Sounds fun, right? But the challenge lies in the actual implementation. How do we keep track of the added and removed components? How do we ensure a seamless experience? The answers to these questions lie in the interplay of our models, controllers, and views, together with a very simple reflex. Let's dive in!

Setup: Models, Controllers, and Views

Before we see how StimulusReflex acts as our magic wand, let’s first understand the architecture of our sample app. Let's assume we have a grid of cells, and we want our users to be able to add and remove cells themselves.

The GridController

First we need a boilerplate controller, the GridController (app/controllers/grid_controller.rb), which fetches all cell records. These cells are the building blocks our users will use to dynamically assemble their UI. Note that in our example here, these cells represent all persisted elements of the grid:

class GridController < ApplicationController
  def index
    @cells = Cell.all
  end
end

Models

The Cell model in app/models/cell.rb comes with a unique attribute, uuid, that identifies each cell. This is necessary to idenfity template cells, i.e. those that have not yet been persisted.

class Cell < ApplicationRecord
  attribute :uuid, :string
end

But, wait. We also have the User model in app/models/user.rb, which keeps track of the templates added by the user. In our example here, we just use a plain array to store them.

class User < ApplicationRecord
  attr_accessor :templates
  
  def initialize
    super
    @templates = []
  end
end

Remember that @templates will hold yet unpersisted Cell instances.

Our Views: Constructing the UI

In the app/views/cells/index.html.erb view, we combine the cells and the user's unsaved templates. We also provide an "Add Cell" button that makes use of the StimulusReflex magic to add cells dynamically.

<div class="grid">
  <%= render @cells %>
  <%= render current_user.templates %>
  <%= tag.a "Add Cell", class: "btn btn-primary", data: { reflex: "click->Template#insert" } %>
</div>

For each individual cell, there's a close button that removes it from the view, as seen in app/views/cells/_cell.html.erb:

<div class="cell">
  <button type="button" class="btn-close" aria-label="Close" data-reflex="click->Template#remove" data-uuid=#{cell.uuid}></button>
</div>

Note: Not implemented in this example is the actual saving of template cells.

How StimulusReflex Fits In

With the structure in place, you might wonder, where does StimulusReflex come into the picture? Well, it is the bridge between your user's actions and the dynamism of your UI.

class TemplateReflex < ApplicationReflex
  def insert
    current_user.templates << Cell.new(uuid: SecureRandom.urlsafe_base64)
  end

  def remove(uuid = element.dataset.uuid)
    current_user.templates.delete_if { |template| template.uuid == element.dataset.uuid }
  end
end

When a user clicks the "Add Cell" button, the insert method gets triggered, adding a new cell. And, when the close button on a cell is clicked, the remove method ensures the cell gets removed. All of this happens without any page reloads, making the experience super fluid for the user!

This is made possible because of the paradigm of StimulusReflex: In essence, the current controller action is rerendered, but with an altered state. You can think of the reflex as a before_action which exposes an interface for you to change the state. Afterwards, the controller action is invoked and the HTML streamed to the client and morphed using morphdom. Here is a live sandbox of this example:

Caveats and Variations

  • "Template" Backend: In this setup, template cells are stored in memory, which means they are temporary. In a real-world application, you'd likely want to persist these to avoid data loss, e.g. in a key-value store like Redis.

  • UUID Generation: We use SecureRandom.urlsafe_base64 for generating UUIDs, ensuring they are unique. If your application has different requirements, you might need a different strategy.

  • Real-time Feedback: One of the biggest advantages of implementing Composable UI using StimulusReflex is the real-time feedback it offers. But always ensure your reflexes are optimized to avoid any performance bottlenecks.


In conclusion, the beauty of composable UI using StimulusReflex is that it provides developers with a structured yet flexible way to build dynamic interfaces. By understanding the intricate dance between models, views, and controllers, and the power of StimulusReflex, you can truly revolutionize the user experience. Happy coding! 🚀

Supercharge Your Rails App

Upgrade Your Tech Stack for a Smoother Dev Experience and Increased Profits.

Order a Review