Building Reactive Rails applications with StimulusReflex

Introduction

Rails is a highly productive, highly opinionated, and very popular framework in which to create monolithic web applications. Due to the dominance of reactive Javascript frameworks such as React and Angular, the monolithic style of creating web applications has lost a bit of popularity in recent years.

Reactive Javascript frameworks tout the advantage of facilitating lightning-fast UI interactions - changing only bits and pieces of the DOM in response to changes in client-side state without the need for a slow HTTP request/response cycle. These frameworks live up to their promise of creating great user experiences, but managing client-side state has a lot of challenges:

  • "Hydrating" the client's state from the server
  • Where do we put our state? In one huge store, or spread out among javascript components?
  • Since we are pulling our client's data from a server (API), we now have 2 sources of truth: The server's state and the client side state in the web browser. Now we have the problem of synchronizing those two states.

Many of these problems are solved by libraries such as Redux, but this also comes with the cost of adding extreme complexity to your application. Following the KISS Principle, we as developers generally want to reduce complexity and strive to create amazing experiences that embrace simplicity.

StimulusReflex is a way to create snappy, reactive user interfaces using the "Rails way" that you already know and love.

Inspired by Phoenix LiveView, StimulusReflex bypasses the complexities of managing client state by keeping state on the server and pushing server-rendered DOM changes to the client over websockets. This always-connected model of rendering server-side then pushing diffed DOM changes allows the developer to write applications that store state on the server, yet feel like single page applications.

Note that StimulusReflex does not aim to replace React, Vue, Angular, or other frontend frameworks; It simply aims to be an alternative way to write applications that are much more reactive and interactive than traditional Ajax/XHR/Server-Side apps.

Do you miss the days of creating majestic monoliths that avoid the complexity of synchronizing client-side and server-side state? Then let's get started!

Let's build something simple. Since todo apps seem to be the gold standard for Javascript framework tutorials, let's go with that. In this tutorial, I assume that you know Rails, basic Javascript, and have some knowledge of how databases work.

The source code for our finished app is here. Feel free to pull this repo and follow along if you get stuck. Here's a look at our finished app:

Reactive Todo App

Prerequisites

  • Ruby (2.5.0 or newer)
  • Redis (For ActionCable/CableReady)
  • Basic Rails knowledge
  • Basic Javascript knowledge

One minor gotcha is that StimulusReflex applications require Redis, and many people do not have a Redis instance running on their machine. A very easy way to spin up a throwaway redis instance is to just use Docker:

docker run -p 6379:6379 redis

Create a new Rails app

StimulusReflex builds on the Stimulus JavaScript library from Basecamp and uses CableReady to push updates to the DOM over websockets.

Here, we tell Rails to create a new application and to install the Stimulus library with Webpack.

rails new todo --webpack=stimulus

Install StimulusReflex

cd todo
bundle add stimulus_reflex
bundle exec rails stimulus_reflex:install

Note that Stimulus Reflex adds a couple of directories to your default Rails install:

app/javascript/controllers
app/reflexes/

app/javascript/controllers is where your Stimulus controllers live. StimulusReflex extends the functionality of Stimulus controllers in application_controller.js. There is also an ExampleController and ExampleReflex created in these directories, respectively. Feel free to give them a quick look, as they explain a little bit about how Stimulus controllers and Reflexes interact with each other.

Create a Todo List

Create a model and controller

rails g model TodoItem title:string done_at:datetime
rails g controller TodoItems

We're keeping things simple, rendering the TodoItemsController#index method only and letting our reflexes take care of most of the CRUD. This might be confusing for someone who's written a lot of Rails apps, but it will become clear later. For now, let's just assume that the only thing we need rendered is the Todo Items index.

Edit app/models/todo_item.rb to have the following

class TodoItem < ApplicationRecord
  scope :unfinished, -> { where(done_at: nil) }
  scope :finished, -> { where("done_at IS NOT NULL") }

  validates_presence_of :title
end

Change our controller to look like this

class TodoItemsController < ApplicationController
  def index
    @finished_items = TodoItem.finished.order('done_at DESC')
    @todo_items = TodoItem.unfinished.order('created_at DESC')
  end

  private

  def todo_item_params
    params.require(:todo_item).permit(:title, :done_at)
  end
end

Now it's time to create our views. In this example, I'm going to be using tailwind via CDN because it's super simple to integrate into our app with minimal headaches.

Open app/views/layouts/application.html.erb and add the following

<!DOCTYPE html>
<html>
  <head>
    <title>Todo List</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <!-- Add the line below -->
    <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body class="min-h-screen bg-gray-100">
    <div class="container mx-auto">
      <%= yield %>
    </div>
  </body>
</html>

Note that we are bypassing webpack and the asset pipeline with this, and this is not the usual way of adding stylesheets to your application. We're simply doing this out of convenience and to not have to go off on a tangent for this tutorial.

Create a view in app/views/todo_items/index.html.erb

<div class="max-w-none mx-auto mt-5">
  <div class="bg-white overflow-hidden shadow sm:rounded-lg text-xl">
    <div class="p-5">
      <%= form_for(TodoItem.new, class: "mt-2") do |f| %>
        <%= f.text_field :title,
                      autofocus: true,
                      class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-900 leading-tight focus:outline-none focus:shadow-outline",
                      placeholder: "What needs to be done?" %>
      <% end %>
      <div class="text-gray-600">
        <ul class="mt-5" aria-disabled="true">
          <% @todo_items.all.each do |todo_item| %>
            <%= render todo_item %>
          <% end %>
        </ul>
        <% if @finished_items.any? %>
          <div class="border-t-2">
            <h3 class="my-2">Done:</h3>
            <ul>
              <% @finished_items.all.each do |finished_item| %>
                <%= render finished_item %>
              <% end %>
            </ul>
          </div>
        </div>
      <% end %>
    </div>
  </div>
</div>

Create a partial in app/views/todo_items/_todo_item.html.erb

<li class="py-4 px-2 hover:bg-gray-50 border-t border-gray-300">
  <div class="flex justify-between">
    <div>
      <%= check_box_tag "todo_item_item[done_at]", "1",
                  todo_item.done_at.present?,
                  id: "todo_item-done-#{todo_item.id}" %>
    </div>
    <div class="w-4/5 <%= todo_item.done_at ? ' line-through' : '' %>"><%= todo_item.title %></div>
    <div>
      <%= link_to todo_item, method: :delete do %>
        &times;
      <% end %>
    </div>
  </div>
</li>

Let's make our todo list the default route:

Edit config/routes.rb to look like this

Rails.application.routes.draw do
  resources :todo_items
  root to: 'todo_items#index'
end

Migrate the database and start the rails server:

rails db:migrate
rails server
open http://localhost:3000

You should see our application. Running in the browser. It will not do much until we create a few reflexes.

Add reflexes to the Todo list

From the official StimulusReflex documentation: "A reflex is a full, round-trip life-cycle of a StimulusReflex operation, from client to server and back again". You may ask: "Isn't that just like AJAX? Isn't that… slow?". The answer would be "no", since reflexes happen over pre-established websocket connections, avoiding the overhead of HTTP requests. In addition, StimulusReflex uses "morphs" to intelligently update the DOM, only sending over the wire what needs to change.

The cool thing about all of this is that it allows you to create Rails applications the usual way while creating richly interactive and fast user experiences.

Reflexes can be either invoked right from your view via data attributes, or in a Stimulus controller. We'll be using both methods and I'll show you why in just a moment.

Let's create a TodoItem reflex:

rails generate stimulus_reflex TodoItem

This will create app/reflexes/todo_item_reflex.rb and app/javascript/controllers/todo_item_controller.js, our Reflex and Stimulus controller respectively.

Wire up Stimulus to Create a Todo Item

Here is where the magic happens. Let's wire up a reflex action to handle creating todo items. The easiest and quickest way to create reflexes is to declare a direct reference to the reflex in the data attributes of a HTML element. Unfortunately we can't do that here because we need to override the default submit behavior of our form in Javascript (i.e.: preventDefault()).

In cases like these, we have to trigger the reflex from the Stimulus controller

Open app/javascript/controllers/todo_item_controller.js and add the following methods

create(event) {
  event.preventDefault();

  // This triggers our Reflex...
  this.stimulate("TodoItem#create");
}

createSuccess(element, reflex, error, reflexId) {
  document.querySelector("form").reset();
}

Add this to app/reflexes/todo_item_reflex.rb

def create
 TodoItem.create!(todo_item_params)
end

private

def todo_item_params
  params.require(:todo_item).permit(:title)
end

Note that params here is an instance of the ActionController::Parameters class that we know and love, therefore we have to permit the title param to be assigned.

In order to wire up the the form (app/views/todo_items/index.html.erb), we have to use data attributes to tell the Stimulus library to handle the submit event.

Handle the form submit in the Stimulus action

<%= form_for(TodoItem.new, class: "mt-2",
             data: {controller: "todo-item", action: "submit->todo-item#create"}) do |f| %>

Note that the StimulusReflex documentation states that we shouldn't be handling form submits in Reflex actions. For this example, we're going to ignore that bit of advice for now and place our form submit handler in the reflex action. In production applications, check out the Working with HTML forms section of the official documentation and decide what approach makes the most sense for your application.

NOTE: Whenever you create or change a reflex or Stimulus action, you must reload the page in your browser to pick up those changes.

Let's try out our form. You should be able to type in a todo item, hit enter, and see your item automagically appear in the todo list. Try adding a few items, noticing how fast it is.

Marking a Todo Item as done

Now that we have a few todo items, it would be nice to mark them as done.

Add the following method to app/reflexes/todo_item_reflex.rb (after the create method)

def toggle_done
   todo_item = TodoItem.find(element['data-id'])
   toggle = todo_item.done_at ? nil : DateTime.now
   todo_item.update_attribute(:done_at, toggle)
end

Wire up a Reflex action to fire when a user clicks on the checkbox next to the todo item

In app/views/todo_items/_todo_item.html.erb:

<%= check_box_tag "todo_item_item[done_at]", "1",
            todo_item.done_at.present?,
            id: "todo_item-done-#{todo_item.id}",
            data: {
              reflex: "change->TodoItemReflex#toggle_done",
              id: todo_item.id } %>

We don't need to override event handling for the checkbox element to it's easiest to just call the reflex directly from our view with data attributes. Go ahead and click the checkbox next to one of your Todo items and see what happens. Fast, huh?

Triggering a Reflex method from data attributes uses the following nomenclature: [DOM-event]->[ReflexClass]#[action]. Here we use change->TodoItemReflex#toggle_done to toggle the done state of our item on change event of the checkbox.

Deleting a Todo Item

def destroy
  TodoItem.find(element['data-id']).destroy
end

Deleting a todo item is very similar to marking one as done. We're going to add data attributes to trigger the Reflex action directly:

<%= link_to todo_item, method: :delete,
          data: {
          reflex: "click->TodoItemReflex#destroy",
          id: todo_item.id } do %>
  &times;
<% end %>

This works similar to the checkbox, except that we're triggering the destroy method of the reflex on the click event.

Now we're able to create, mark as done and not done, and destroy our todo items. As you click around, notice how fast this is. What amazes me is how this is to set up, requiring no specialized knowledge of frontend frameworks. We are able to create fast, reactive Rails apps without the trouble of writing a million lines of Javascript on the frontend, nor do we have to sync state with a backend API. We simply create a regular Rails app, then enhance it with Reflexes.

Conclusion

StimulsReflex is not a replacement for reactive Javascript applications and it doesn't try to be, but it's easy to see that there are alternatives to creating complicated single page applications. StimulusReflex is a great way to leverage your teams Rails knowledge to build rich user interfaces that sidestep the need for complex state management libraries.

StimulusReflex is pretty young, but just by writing a simple application we can see how it's easy to become productive in writing reactive apps very quickly. Now that you've gotten your feet wet, a great next step would be to check out the Twitter clone demo by Nate Hopkins, the creator of StimulusReflex.

Image Attributions

Photo by Preston Goff on Unsplash