StimulusReflex with Rails is AMAZING

📅 December 04, 2020

👷 Chris Power

stimulusreflexlogo

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.

How Does StimulusReflex Work?

StimulusReflex basically works like this:

  • User performs some action.
  • The action is passed through a Stimulus controller (kind of magically)
  • A “Reflex” (think Ruby version of a Stimulus controller) updates the state of the app. Creating a new comment, updating an object in ActiveRecord, etc…
  • The page is re-rendered, and diffed (essentially) using the morphdom library.
  • The diffed DOM is passed through ActionCable using CableReady
  • The updated diff gets displayed on the page without flickers or hiccups.

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.

It's mindblowing

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

But why use this instead of React?

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:

1. Javascript is annoying

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.

  • Ask yourself, do you enjoy working with webpack?
  • How about npm, or yarn?
  • How about the fact that you need a dozen packages, JUST to get the functionality that most standard libs give you out of the box?
  • ES6 is nice, so is ESNext, but you’ll need a transpiler to bridge the gap between it all.
  • Not to mention you’re compiling/transpiling for a crazy amount of browsers, and different browsers have different kinks, eccentricities, etc…

2. React can be difficult to work with

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).

3. Testing in React leaves something to be desired

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!

Ok, Ok, I’ve had enough React bashing, show me an example.

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.

Preface — “LivingRoom” the app

livingroom icon

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.

The example: A filterable table.

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.

filterable table

Filterable tabe in React

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.

And with StimulusReflex

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!

There is some power with this.

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.

Lets Work Together

We're trusted by large, medium, and small companies all over the world

Have something you're working on?

Tell Us About It