A while ago I made a blog post about the modern web with just Rails (and a few other things). Today I'm going to explore another way of doing awesome things with Rails, in the spirit of the modern, reactive, and real-time, web. Let's explore StimulusReflex, an extension to the amazing library made by Basecamp, to make server-side reactive applications.
So what is this Stimulus Reflex thing?
StimulusReflex is a Rails library that allows developers to make reactive, real-time apps easily. Much like Phoenix LiveView on Elixir land, this library gives us tools to make reactive UIs running mostly on the server-side.
It uses ActionCable, and their own CableReady extension, to trigger DOM updates from the server-side. It uses WebSockets, so it's faster than using regular HTTP requests with say, Turbolinks, or just consuming a regular API. Imagine kinda like Turbolinks, but sending HTML via WebSockets, instead of regular requests.
StimulusReflex adds the ability to write reflexes. Pieces of code that execute asynchronously and trigger a DOM update. Imagine a React component and some callback that triggers a
setState call. These reflexes are fully server-side, so you can read directly from the database and do all kinds of things you would do on a typical Rails controller. You then attach DOM events to these reflexes with
data attributes, kinda like Stimulus. You can also call these reflexes from Stimulus controllers directly.
Let's dive right into the action. Imagine a view that renders a group of
todo records. Then we want each todo to have a delete button that removes that todo without refreshing the webpage.
Here is the controller:
This is the view:
Then our reflex becomes:
You use the
element variable, available on all reflexes, to get the DOM element where we define our
data-reflex (our button). Then we get our
data-todo_id from it.
We then find the todo on the database and delete it. When the method returns, StimulusReflex uses the current controller action to re-render the page with the new info. Then, the new HTML is sent via WebSockets and merged with the previous DOM contents.
You could replace this functionality with a simple
remote: true Rails form, but as this is submitted with Websockets, the UI update is much faster and gives user's faster feedback, making the app feel snappier.
How the magic happens
As I said earlier, StimulusReflex uses CableReady to trigger DOM updates from the server side. It's a library developed by the same authors, and you can use it to do some more low-level stuff. It essentially sends DOM instructions through ActionCable, which is a WebSocket library built into Rails.
The magic happens in those reflex classes. When you trigger a reflex, StimulusReflex essentially rebuilds the current page, calling your controller action again, and it sends that new HTML via the ActionCable socket. Then, the client-side StimulusReflex library morphs the updated DOM with morphdom. By default, it updates the entire page, but you can reduce the scope of the updates manually to increase performance, check their docs for more info.
This is a very quick primer on this amazing library. Their docs explain all of this in detail, plus some advanced use cases. It also contains all the instructions to deploy this to production without slowing down the entire application!
The classic todo app
So let's revisit that example I showed you earlier. That example called reflexes directly on the markup. It's a quick way of doing it, but the best way is through a Stimulus controller itself. It gives you more control, allowing you to do cooler things.
The objective here is to make a simple todo list that allows you to create and delete todos. We are doing this entirely on
StimulusRefex to achieve instant feedback without any full-page refreshes whatsoever. So, the only thing our Rails controller does is rendering the page.
By the way, I'm assuming you already made the Stimulus and StimulusReflex setup steps, and that you have a model and associated database migration, for a
Todo with a
description text field.
So, our markup:
The important here is the
data attributes. Firstly, we annotate the top-level
data-controller="todo" which tells Stimulus that it should attach the
todo_controller.js to this markup. Then we just define two actions.
click->todo#delete on the button, to delete todos, and
submit->todo#create to submit the form and create todos. Notice that on the delete button we also set
data-todo_id="<%= todo.id %>" to so the reflex knows what todo it should delete.
Now, our Stimulus controller:
Here we can see the StimulusReflex
stimulate method. It allows you to call reflexes from Stimulus controllers. We pass the
event.currentTarget so our reflexes have access to the DOM element where the event handler was specified (the button and the form).
We also have two methods to show a little loading spinner! The
stimulate method returns a promise, so you can do things before and after and keep everything in sync. We also
reset the form after submitting it, which doesn't happen automatically because we are canceling the default events so we don't submit a real
Finally, our reflex:
delete function remains unchanged, but notice the cool thing about the
create. Because the DOM element that triggered the event is a form, we can use the classic Rails param mechanisms, so the code looks like a classic Rails controller.
This is everything you need to render a list of todos and also delete and create them as you wish. The styling with CSS is not part of this blog post scope, I'll leave that up to you.
But, in short, if your app has authentication, you need to identify each ActionCable connection with the currently logged-in user, otherwise, the users will see each other's updates because they will all share the same socket.
Also, you need to configure your app to use Redis as a cache in a production environment. I recommend doing this in all environments. It's a good practice for your dev environment to be as similar as possible to your production environment.
Finally, if you are worried about Rails' ability to scale, just use AnyCable. It boosts Rails' ActionCable by delegating socket management to a piece of software written in Go which is miles better than Ruby for this workload. Rough benchmarks show that a Rails instance with ActionCable might handle 4000 connections while a single AnyCable node might handle 10000.
There you go, another cool way of making reactive interfaces without SPAs. It's a very nice manner of adding bits of reactivity to existing Rails' apps.
We are using it for internal projects, trying to add some interactivity on some classic Rails apps.
I hope this has been useful in some way, it's not a detailed tutorial but more of a showcase of another interesting out-of-the-box technology. If you are interested in this sort of thing (alternatives to SPAs) but are afraid of jumping into Ruby because of performance concerns (an overrated concern), checkout Phoenix LiveView. It's written in Elixir, which runs on the BEAM. Performance will not be a problem whatsoever, and... we might have a blog post coming about that soon ;)