A Future for Rails: StimulusReflex
What's in store for the future of Rails development? Andrew Stuntz share's his thoughts on what's next for Ruby on Rails and his experience using StimulusReflex to build a simple chat application.
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](https://dhh.dk/)
It's easy to say that most Rails developers are anxious around JavaScript. This led to the rise of [Coffeescript](https://coffeescript.org/), [Sprockets](https://rubygems.org/gems/sprockets/versions/3.5.2), 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](https://stimulusjs.org/handbook/origin). It allows you to use modern Javascript on your Rails projects without having to go outside the "rails way".
## 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](https://phoenixframework.org/) and [Elixir](https://elixir-lang.org/) community, Chris McChord introduced the world to [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html), a whole new paradigm for serving and updating HTML from your backend.
At its core, LiveView is able to leverage [Websockets](https://www.html5rocks.com/en/tutorials/websockets/basics/) 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](https://hey.com/) was built with some sort of frontend updating system that builds on Stimulus, [Turbolinks](https://github.com/turbolinks/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](https://docs.stimulusreflex.com/) which builds on the Stimulus framework and a gem called [CableReady](https://cableready.stimulusreflex.com/) 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.
## 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 set up, 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:
-- CODE line-numbers language-rb --
<!--
class ChatController < ApplicationController
skip_authorization_check
def index
render locals: { messages: Message.all.order(created_at: :desc) }
end
end
-->
Then consume those messages in the chat index view:
-- CODE line-numbers language-erb --
<!--
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
-->
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.
-- CODE line-numbers language-erb --
<!--
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" }
-->
In `app/javascript/controllers/chat_controller.js` you should create a `create_message` handler that handles the click event from your `Message` button.
-- CODE line-numbers language-jsx --
<!--
import { Controller } from 'stimulus';
import StimulusReflex from 'stimulus_reflex';
export default class extends Controller {
create_message(event) {
event.preventDefault()
console.log('Clicked')
}
}
-->
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](https://docs.stimulusreflex.com/reflexes#declaring-a-reflex-in-html-with-data-attributes) in the StimulusReflex docs.
To connect this to our Reflex we need to import the StimulusReflex module and register it in our Controller.
-- CODE line-numbers language-jsx --
<!--
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)
}
}
-->
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.
-- CODE line-numbers language-rb --
<!--
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
-->
See the connection delegation at the top of the class? We need to be sure to expose that to our connection.
-- CODE line-numbers language-rb --
<!--
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
-->
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:
### 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`.
-- CODE line-numbers language-rb --
<!--
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
-->
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:
-- CODE line-numbers language-jsx --
<!--
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')
}
}
-->
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.
-- CODE line-numbers language-rb --
<!--
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
-->
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:
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.
## Where to go from here
I remember building a chat application when [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) 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.