📅 December 04, 2020
👷 Chris Power
Recently, I saw a video posted online about this new emerging technology in the Ruby on Rails world. It is called StimulusReflex. It combines the simplicity of the Stimulus framework, with the power of Websockets (using CableReady), and the ease-of-use of Rails’s conventions.
In this article, I want to higlight StimulusReflex, go over an example use-case, and compare/contrast between using Reflex, and React to solve a problem. Personally, I am extremely excited about StimulusReflex, and I hope that after reading this article, you’ll be excited too.
StimulusReflex basically works like this:
If ☝️ sounds like a lot of magic, and buzzword-y crap to you, I understand where you’re coming from. I also thought this sounded a bit bizzare. However, once I started using these libraries I realized something — it feels like react, but without javascript.
Wait, let me say that again, Ahem…
StimulusReflex is like writing React code, without having to use Javascript.
I’m serious. After using StimulusReflex, I don’t think I’ll use React for a side project ever again. You get the power of Rails’ conventions. The simplicity of Ruby. And the speed of Dom diffing. All without the need for javascript
You see; my dear reader, I think React is an amazing library. In fact, I work as a React dev in my day job. I love React, but there are a few problems with it:
Believe it or not, I really like the concept of Javascript. I think functions as a first-class citizen make the language amazing. I love the concept of passing functions around, writing pure functions without side effects, and currying functions together to make amazingly terse logic. The problem I have with Javascript is The Tooling SUCKS.
Have you ever worked on a large-scale React project? Have you ever worked on a well organized large scale React proejct? I haven’t, and I’ve seen a lot of them.
React always takes the stance that it’s a “library” and not a “framework”. This distinction allows React to delegate the responsibility of project structure to you, the end user. Unfortunately, many people have different opinions on how React apps should be structured. Because of differing opinions, and desperate libraries used to solve different problems (think Redux, Redux-Toolkit, Context, Sagas, etc..), you wind up with a bigger and bigger mess the longer a project lives on.
Rails, on the other hand is a very opinionated framework. It relies on convention over configuration. It’s meant to be used only a single way, and it bakes in solutions for its most simple problems. Even the non-standard problems have widespread community support behind their preferred solutions (think service objects when you have too much logic in your controller, that sort of thing).
Once again, to be transparent, I love React. What I don’t love about React is its testing story. The tooling is just difficult to use, and cumbersome. In my experiences, I have not found a single good soltion for end-to-end testing in React. Sure, Cypress is really great, but it just doesn’t hold a candle to Capybara and Rails. With Rails/Capybara, its just so much simpler to control the data, the schema, and the interactions. I mean shit, even the dom/node querying feels simpler in Capybara compared to Cypress!
Oh right, I mentioned an example…
Personally, I feel like I have a great example to showcase how amazing StimulusReflex is. I have built an app literally 5 different ways, and its not a TODO application, I promise. This will not be some contrived example, its a real-world app that I have spent many years trying to write, slowly going insane in the process.
I have a side project called “LivingRoom”. It is an application that has taken many forms over the years, and I have built, and re-built it half a dozen times. LivingRoom is an application for property managers. You can accept payments from tenants through stripe, manage requests, and do many many more things. I am currently re-building this app to only focus on tenant requests.
In its last iteration, it was built with Ruby on Rails, while using React (with the amazing javascript_pack stuff) for the more complicated bits, like filterable tables. This was an amazing solution, but I felt it was a bit too verbose. When re-writing the app (just to do tenent requests) I figured I’d try StimulusReflex to replace the React stuff. I was amazed at the outcome.
I want to give just one example to showcase the power of StimulusReflex over a typical React setup. The example is a filterable table shown below.
With a filterable table, we want to allow a user to filter some data in a number of different ways. We want dropdowns, and fuzzy search.
first, you’ll need a pack to put React on your page in Rails:
import React from 'react';
import ReactDOM from 'react-dom';
import ResidencyFilterTable from 'components/residencies/ResidencyFilterTable';
document.addEventListener('DOMContentLoaded', () => {
const node = document.getElementById('propertyId');
let propertyId;
if (node !== null) {
propertyId = JSON.parse(node.getAttribute('data-property-id'));
}
ReactDOM.render(
<ResidencyFilterTable propertyId={propertyId} />,
document.getElementById('residency_table'),
);
});
Here are some filters in React:
import React from 'react';
import PropTypes from 'prop-types';
import getProperty from 'util/dynamic-property';
import axios from 'axios';
import ResidencyTable from 'components/residencies/ResidencyTable';
import FuzzySearchFilter from 'components/filters/FuzzySearchFilter';
import Loader from 'components/Loader';
const { Component } = React;
class ResidencyFilterTable extends Component {
constructor(props) {
super(props);
this.state = { residencies: [], filteredResidencies: [], loading: true };
this.fuzzyFilterName = this.fuzzyFilterName.bind(this);
this.fuzzyFilterUnitName = this.fuzzyFilterUnitName.bind(this);
this.fuzzyFilterEmail = this.fuzzyFilterEmail.bind(this);
}
componentDidMount() {
const { propertyId } = this.props;
let url = 'api/v1/residencies';
if (propertyId !== undefined) {
url = `/api/v1/properties/${propertyId}/residencies`;
}
axios.get(url).then((resp) => {
const { residencies } = resp.data;
this.setState({
residencies,
filteredResidencies: residencies,
loading: false,
});
}).catch(() => {
});
}
filterBy(property, e) {
const str = e.target.value;
const pattern = str.replace(/[^a-zA-Z0-9_-]/, '').split('').join('.*');
const matcher = new RegExp(pattern, 'i');
if (str === '') {
this.setState({ filteredResidencies: this.state.residencies });
} else {
const filtered = this.state.residencies.filter(residency =>
matcher.test(getProperty(residency, property)));
this.setState({ filteredResidencies: filtered });
}
}
fuzzyFilterName(e) {
return this.filterBy('user.name', e);
}
fuzzyFilterUnitName(e) {
return this.filterBy('unit.name', e);
}
fuzzyFilterEmail(e) {
return this.filterBy('user.email', e);
}
render() {
let output = null;
if (this.state.residencies.length > 0) {
output = (
<div>
<div className="row mb-4">
<FuzzySearchFilter
id="fuzzyFilterName"
label="Search Name"
filter={this.fuzzyFilterName}
/>
<FuzzySearchFilter
id="fuzzyFilterEmail"
label="Search Email"
filter={this.fuzzyFilterEmail}
/>
<FuzzySearchFilter
id="fuzzyFilterUnit"
label="Search Unit"
filter={this.fuzzyFilterUnitName}
/>
</div>
<ResidencyTable residencies={this.state.filteredResidencies} />
</div>
);
} else if (this.state.loading === true) {
output = <Loader />;
} else {
output = (
<div className="alert alert-warning text-center no-resident-warning">
You do not have any residents associated to this property.
</div>
);
}
return (
<div>
{output}
</div>
);
}
}
ResidencyFilterTable.propTypes = {
propertyId: PropTypes.number.isRequired,
};
export default ResidencyFilterTable;
And here is the code for the table itself:
import React from 'react';
import PropTypes from 'prop-types';
const ResidencyTable = ({ residencies }) => (
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead className="thead-default">
<tr>
<th scope="row">Name</th>
<th scope="row">Email</th>
<th scope="row">Unit</th>
<th scope="row">Status</th>
<th scope="row">Links</th>
</tr>
</thead>
<tbody>
{residencies.map(residency => (
<tr key={residency.id}>
<td>{residency.user.name}</td>
<td>{residency.user.email}</td>
<a href={`/properties/${residency.property_id}/units/${residency.unit.id}`}>
<td>{residency.unit.name}</td>
</a>
<td>
<span className={`badge badge-${residency.badge.class}`}>
{residency.badge.value}
</span>
</td>
<td>
<a href={`/properties/${residency.property_id}/units/${residency.unit.id}/residencies/${residency.id}`}>Show</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
ResidencyTable.propTypes = {
residencies: PropTypes.arrayOf.isRequired,
};
export default ResidencyTable;
And you’ll need a controller to fetch that data
module Api::V1
class ResidenciesController < ApplicationController
before_action :authenticate_property_manager!
def index
@residencies = current_property_manager
.company
.residencies
.eager_load(:user, :unit)
render :index, status: :ok
end
end
end
all of the above code only manages to filter elements on the frontend. If we wanted to filter things through the server, we’d need to call out to the API every time we want to fitler anything.
I like this code. It’s pretty straightforward and easy to understand; however, its pretty verbose with network calls, and it only allows filtering on the frontend. Lets see what a similar filterable table looks like with StimulusReflex:
We’ll start with the filter form.
<nav class="navbar sticky-top navbar-light bg-light">
<form class="form-inline">
<div class="d-flex flex-column justify-content-start align-items-start mr-4">
<label for="property-select" class="ml-1">Property</label>
<select
name="property-select"
class="custom-select mb-3"
data-tenant-filter-id=<%=@tenant_filter.id%>
data-reflex="change->TenantFilterReflex#filter_property"
>
<option
value=''
<%= 'selected' if !@tenant_filter.property_id %>
>
None
</option>
<% Property.all.each do |property| %>
<option
value=<%= property.id %>
<%= 'selected' if property.id == @tenant_filter.property_id %>
>
<%= property.name %>
</option>
<% end %>
</select>
</div>
<div class="d-flex flex-column justify-content-start align-items-start mr-4">
<label for="unit-select" class="ml-1">Unit</label>
<select
name="unit-select"
class="custom-select mb-3"
data-tenant-filter-id=<%=@tenant_filter.id%>
data-reflex="change->TenantFilterReflex#filter_unit"
>
<option
value=''
<%= 'selected' if !@tenant_filter.unit_id %>
>
None
</option>
<% Unit.all.each do |unit| %>
<option
value=<%= unit.id %>
<%= 'selected' if unit.id == @tenant_filter.unit_id %>
>
<%= unit.name %>
</option>
<% end %>
</select>
</div>
<div class="d-flex flex-column justify-content-start align-items-start mb-3 mr-3">
<label for="fuzzy-search" class="ml-1">Name</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-search"></i></span>
</div>
<input
type="text"
name="fuzzy-name-search"
class="form-control"
data-controller="tenant-filter"
data-tenant-filter-id="<%=@tenant_filter.id%>"
data-action="input->tenant-filter#fuzzyNameSearch"
>
</div>
</div>
<div class="d-flex flex-column justify-content-start align-items-start mb-3">
<label for="fuzzy-search" class="ml-1">Email</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-search"></i></span>
</div>
<input
type="text"
name="fuzzy-email-search"
class="form-control"
data-controller="tenant-filter"
data-tenant-filter-id="<%=@tenant_filter.id%>"
data-action="input->tenant-filter#fuzzyEmailSearch"
>
</div>
</div>
</form>
</nav>
<div class="card-body">
<%= render(TenantTableComponent.new(tenants: @tenants)) %>
</div>
In the ☝️ code, we are setting up a basic form with a couple dropdowns, and a couple inputs. The html form’s have data-elements on them that specify the controller and actions we are taking in Stimulus and StimulusReflex.
And now the Reflex that handles all these filter events:
class TenantFilterReflex < ApplicationReflex
def filter_property
value = element.value
find_tenant_filter(element)
if value == ''
@tenant_filter.update!(property_id: '')
else
@tenant_filter.update!(property_id: value)
end
end
def filter_unit
value = element.value
find_tenant_filter(element)
if value == ''
@tenant_filter.update!(unit_id: '')
else
@tenant_filter.update!(unit_id: value)
end
end
def fuzzy_name_search
find_tenant_filter(element)
if element.value == ''
@tenant_filter.update!(tenant_name_search: '')
else
@tenant_filter.update!(tenant_name_search: element.value)
end
end
def fuzzy_email_search
find_tenant_filter(element)
if element.value == ''
@tenant_filter.update!(tenant_email_search: '')
else
@tenant_filter.update!(tenant_email_search: element.value)
end
end
private
def find_tenant_filter(element)
@tenant_filter = TenantFilter.find(element.dataset['tenant-filter-id'])
end
end
In our “Reflex”, we are updating a @tenant_filter
. This filter is stored in our DB, making is extremely easy for a user to save filters, and come back to them later. Whenever an action is taken, we update the filter, the data gets stored, and the HTML gets generated with the new filtered data. of course, you don’t have to store the filters in your DB. You could save filters in a session variable, or some other cache.
At the bottom of the tenant_filter.html.erb, we render a tenant filter component, here it is:
class TenantFilterComponent < ViewComponent::Base
def initialize(tenant_filter:)
@tenant_filter = tenant_filter
@scope = Tenant.includes(unit: :property)
.all
.order(created_at: :desc)
.references(:units)
@scope = filter_property(@scope)
@scope = filter_unit(@scope)
@scope = fuzzy_search_name(@scope)
@scope = fuzzy_search_email(@scope)
@tenants = @scope
end
private
def filter_property(relation)
if @tenant_filter.property_id
return relation.where('units.property_id = ?', @tenant_filter.property_id)
end
relation
end
def filter_unit(relation)
if @tenant_filter.unit_id
return relation.where(unit_id: @tenant_filter.unit_id)
end
relation
end
def fuzzy_search_name(relation)
if @tenant_filter.tenant_name_search.present?
return relation.where("LOWER(tenants.first_name) LIKE ? OR LOWER(tenants.last_name) LIKE ?",
"%#{@tenant_filter.tenant_name_search.downcase}%",
"%#{@tenant_filter.tenant_name_search.downcase}%"
)
end
relation
end
def fuzzy_search_email(relation)
if @tenant_filter.tenant_email_search.present?
return relation.where("LOWER(tenants.email) LIKE ?", "%#{@tenant_filter.tenant_email_search.downcase}%")
end
relation
end
end
Now, without any network requests, we are filtering data directly on the server, and updating it in real-time on the client’s browser!
I think there is considerable power with the StimulusReflex approach. With StimulusReflex, you’re in total control of the data. You’re rendering the HTML with data straight from the server, without having to make a network call to fetch, or update data. The complete round trip is very quick and controlled. And you get all the goodies that everyone wants, server side rendering, minimal dependencies, and easy to understand code!
This has been my personal experience. And I know I may seem biased, but I think this has a lot of potential.
We're trusted by large, medium, and small companies all over the world
Have something you're working on?
Tell Us About It