DEV Community

Andrew Stuntz
Andrew Stuntz

Posted on

A Future for Rails: StimulusReflex

If you've been in the rails community for any length of time, you've probably heard about the desire to stay away from JavaScript.

"I like JavaScript, but I don't like it that much."
David Heinemeier Hanson, Creator of Ruby on Rails

It's easy to say that most Rails developers are anxious around JavaScript. This led to the rise of Coffeescript, Sprockets, and a whole suite of tools to allow Ruby on Rails developers to build amazing applications without having to write JavaScript.

As Rails continues its path to being a larger and more complete framework, "batteries included," there have been further shifts in the community. Included with Rails is now a more modern JavaScript packager (though Sprockets is still around if you want to use it). DHH also introduced us to a whole new JavaScript Framework called Stimulus. It allows you to use modern Javascript on your Rails projects without having to go outside the "rails way".

‍‍two developers collaborating on project in Phoenix LiveView

Phoenix, Elixir, and LiveView

As other communities have popped up to build "rails inspired" frameworks, many of those frameworks emerged as formidable competitors to single-page applications. In the Phoenix and Elixir community, Chris McChord introduced the world to LiveView, a whole new paradigm for serving and updating HTML from your backend.

At its core, LiveView is able to leverage Websockets and update the Document Object Model (DOM) based on responses from the WebSocket connection. These frameworks are fast enough to feel "real-time".

The promise of LiveView is a return to the early days of Ruby on Rails, where we didn't have to worry so much about JavaScript and we could happily focus on our business logic. Everything else to build a robust CRUD application was taken care of using "rails magic."

Some people in the Rails community have jumped ship to the Elixir/Phoenix community. To be fair, I personally think that for most problems, Phoenix is an amazing solution.

Can Rails still compete?

Rails is nothing to sneeze at and the Rails community has taken steps towards being real-time as well. DHH recently tweeted and to let folks in the community know that much of Hey was built with some sort of frontend updating system that builds on Stimulus, Turbolinks, and the "rails way". I'm excited to hear more.

There have been other efforts to push Rails in the "real-time" direction as well. Recently I came upon StimulusReflex which builds on the Stimulus framework and a gem called CableReady that enables you to build "reflexes" that work much like LiveView does in Phoenix: sending DOM updates over a WebSocket to update the UI.

StimulusReflex could be a possible future for Rails development. I'd like to share my experience building an application with StimulusReflex.

rails chat app example on MacBook pro screen

Creating a chat app with StimulusReflex

I recently wrote a chat application that is fast, responsive, and was simple to build while leveraging StimulusReflex. I also wrote a total of 24 lines of JavaScript - you can't beat that.

Setup

It's easy to get started with StimulusReflex. It's a gem and can be installed in your Rails app by running:

bundle add stimulus_reflex

To get it up and running, simply run the install scripts:

bundle exec rails stimulus_reflex:install

This generates a couple of new starter files and directories. Including the app/reflexes directory. You'll see an example_reflex.rb and an application_reflex.rb.

You also get some JavaScript out of the box, and you'll have a newly created app/javascript/controllers/application_controller.rb and an app/javascript/controllers/example_controller.rb.

The reflexes are where the magic happens. According to the documentation, a "reflex" is a "full, round-trip life-cycle of a StimulusReflex operation - from client to server and back again. The JavaScript controllers are Stimulus Controllers and StimulusReflex is designed to hook right into Stimulus and other Rails paradigms.

Up and Running

Now that we have StimulusReflex setup, let's go ahead and start setting up our chat application. I'm going to make some assumptions.

You have a Rails application that is using Devise.

You have a Message model that includes a message and user_id attribute.

Also, I won't go into the minutiae of writing the views or the styling for the application. If you're curious, I'll link out to the entire code base for you to see what I wrote.

The first thing I did was build out a link to a view that will render our reflex enabled HTML. There is no special controller needed here, I called mine the ChatController and linked to it from my routes.rb as get /chat to: "chat#index".

Here I expose my messages to the view:

class ChatController < ApplicationController
  skip_authorization_check

  def index
    render locals: { messages: Message.all.order(created_at: :desc) }
  end
end
Enter fullscreen mode Exit fullscreen mode

Then consume those messages in the chat index view:

div.flex.flex-col.mx-4
  div.container.flex.flex-col-reverse.overflow-scroll.bg-white.mt-4.rounded
    - messages.map do |message|
      div
        = message.message
Enter fullscreen mode Exit fullscreen mode

You should now have a nice list of messages in your view - easy peasy.

The next step is to add in some Stimulus to create messages. We're just going to get the button click working with Stimulus right now. I added a button with a Stimulus data attribute to create_message in the Chat controller.

div.flex.flex-col.mx-4 data-controller='chat'
  div.container.flex.flex-col-reverse.overflow-scroll.bg-white.mt-4.rounded
    - messages.map do |message|
      div
        = message.message
  div.mb-4
    = form_with model: @message do |form|
      div.mb-4.rounded-md
        = form.label :message
        = form.text_field :message
        = form.submit "Message", data: { action: "click->chat#create_message" }
Enter fullscreen mode Exit fullscreen mode

In app/javascript/controllers/chat_controller.js you should create a create_message handler that handles the click event from your Message button.

import { Controller } from 'stimulus';
import StimulusReflex from 'stimulus_reflex';

export default class extends Controller {
  create_message(event) {
    event.preventDefault()
    console.log('Clicked')
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when you click the Message button you should see 'Clicked' in the JavaScript console. I love Stimulus that is easy. You don't actually have to use Stimulus here - you can add data attributes to your HTML that will trigger reflex actions straight from your HTML.

See the note here in the StimulusReflex docs.

To connect this to our Reflex we need to import the StimulusReflex module and register it in our Controller.

import { Controller } from 'stimulus';
import StimulusReflex from 'stimulus_reflex';

export default class extends Controller {
  connect() {
    StimulusReflex.register(this)
  }

  create_message(event) {
    event.preventDefault()
    this.stimulate('Chat#create_message', event.target)
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when we click the button you should see a big fat error saying that the ChatReflex module does not exist. Now let's create the ChatReflex and handle our create_message action.

class ChatReflex < StimulusReflex::Reflex
  delegate :current_user, to: :connection

  def create_message
    # Create the message
    Message.create(message: params["message"], user_id: current_user.id)
  end
end
Enter fullscreen mode Exit fullscreen mode

See the connection delegation at the top of the class? We need to be sure to expose that to our connection.

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected

    def find_verified_user
      if current_user = env["warden"].user
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

We identify our Chat reflex connection by the current_user which uses the current_user.id to separate connections. We also add a layer of Authentication here that allows us to pick up our user from Devise when we connect to our WebSocket.

After this is created, when you click your Message button, you should now see the message returned back to view and update in real-time. It's really fast, and also super easy.

Summary of where we are right now:
summary flow chart

Bringing it all together

So now you can connect two users to the chat room and attempt to chat with one another. But there is a small problem - you only get the other users' messages when you send a message or refresh the page. That's not very real-time at all.

Currently, the view is only re-rendered and broadcast to the current_user on that connection. But, we can leverage ActionCable to tell StimulusReflex that there are new messages that need to be rendered. Easy enough.

To achieve this, we spin up a new ActionCable channel. I called it the ChatChannel.

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end
Enter fullscreen mode Exit fullscreen mode


This allows us to connect to a Chat room outside of the StimulusReflex paradigm and we can broadcast back up this channel that the users that are attached need to get the new messages.

To accomplish this, we add some JavaScript to initiate that connection:

In your Stimulus chat controller the initializer connects to the ChatChannel and then binds a function to the received callback of the ActionCable subscription.

It looks like:

import { Controller } from 'stimulus';
import StimulusReflex from 'stimulus_reflex';
import consumer from '../channels/consumer';

export default class extends Controller {
  connect() {
    StimulusReflex.register(this)
  }

  initialize() {
    // We need to know when there are new messages that have been created by other users
    consumer.subscriptions.create({ channel: "ChatChannel", room: "public_room" }, {
      received: this._cableReceived.bind(this),
    });
  }

  create_message(event) {
    event.preventDefault()
    this.stimulate('Chat#create_message', event.target)
  }

  _cableReceived() {
    this.stimulate('Chat#update_messages')
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when we receive a message (any message currently) we trigger the _cableReceived callback which will stimulate the Chat#update_messages actions.

Add an update_messages action to the Chat reflex and then broadcast a message when you create the Message in the create_message action. So, now when any user creates a new Message, we will rebroadcast that message out to anyone that is connected to the chat_public_room connection.

class ChatReflex < StimulusReflex::Reflex
  delegate :current_user, to: :connection

  def create_message
    # Create the message
    Message.create(message: params["message"], user_id: current_user.id)

    # Broadcast that everyone on this channel should get messages
    ActionCable.server.broadcast(
      "chat_public_room",
      body: 'get_messages'
    )
  end

  def update_messages; end
end
Enter fullscreen mode Exit fullscreen mode

Since the view gets re-rendered from the ChatController, we don't actually have to do anything with the messages in the Reflex action. We get to treat the view just like we would have in a Controller Action.

Now you should be able to attach two users and chat back and forth with one another.

The flow through the app looks like:

flow through the app


So we short circuit the button to force stimulus to trigger the Reflex to get everyone's chat's to refresh when a new Message is created.

It's simple and very powerful. The tools that build this application are quite well known and StimulusReflex is built on well-known Rails gems and tools including, Stimulus, CableReady, and ActionCable.

I'm excited to continue exploring how to leverage StimulusReflex in apps we build at Headway, as well as other solutions in this vein including LiveView on Phoenix and Elixir. I believe it will reduce the time we all spend writing single-page applications without losing the real-time feel.

The results so far

Below is the final version of my quick and dirty StimlusReflex chat application.

final chat app results

Where to go from here

I remember building a chat application when ActionCable first launched and thinking the set up was lengthy and took some work. WebSockets were mysterious to me too. StimulusReflex does a great job in hiding that complexity and makes your Rails application feel even more like an SPA. There is such little JavaScript in this version it's not even funny.

More opportunities for this chat application are:

  • Add better Authentication and do some work to add presence notifications

  • Make it so you can create and join any chat room - not just the public room

Get access to the code

What would you add to my chat application next? You can see the final version of the code for this walkthrough here on GitHub. I added some styling and made it look a bit like a messenger application.

Top comments (8)

Collapse
 
asyraf profile image
Amirul Asyraf

Great explanation bro. But the repo is missing. Can you share the repo link ??

Collapse
 
drews256 profile image
Andrew Stuntz

Hey! Absolutely. I didn't realize it was missing.

Collapse
 
drews256 profile image
Andrew Stuntz
Collapse
 
leastbad profile image
leastbad

Andrew, this is a great article. Let me know if you'd like to follow it up with a Part 2 where you get that JS LOC down to 0 with declared reflexes.

Collapse
 
drews256 profile image
Andrew Stuntz

Yeah, that'd be nifty. I haven't played with reflexes enough to get it to zero yet. I'd love to follow up with part 2 with no JS! 🎉 Have any pointers/resources on declared reflexes?

Collapse
 
leastbad profile image
leastbad

I mean, I worked pretty hard to make the documentation pretty solid, so:

docs.stimulusreflex.com/reflexes#d...

We're also waiting to help answer any questions you might have in #stimulus_reflex on discord.gg/XveN625

Thread Thread
 
drews256 profile image
Andrew Stuntz

great!

Collapse
 
rhymes profile image
rhymes

Hi Andrew! cool tutorial. The link to the repository is not there though.

Thank you!