Skip to content
On this page

Useful Patterns

In the course of creating StimulusReflex and using it to build production applications, we have discovered several useful tricks. While it may be tempting to add features to the core library, every idea that we include creates bloat and comes with the risk of stepping on someone's toes because we didn't anticipate all of the ways it could be used.

Client Side

Application controller pattern

You can make use of JavaScript's class inheritance to set up an Application controller that will serve as the foundation for all of your StimulusReflex controllers to build upon. This not only reduces boilerplate, but it's also a convenient way to set up life-cycle callback methods for your entire application.

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

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

  sayHi () {
    console.log('Hello from the Application controller.')
  }
}

You can then create a Reflex-enabled controller by extending ApplicationController:

javascript
import ApplicationController from './application_controller'

export default class extends ApplicationController {
  sayHi () {
    super.sayHi()
    console.log('Hello from a Custom controller')
  }
}

If you need to override any methods on your Application controller, you can redefine them. Optionally call super.sayHi(...Array.from(arguments)) to invoke the method on the parent super class.

Spinners for long-running actions

You can use beforeReflex and afterReflex to create UI spinners for anything that might take more than a heartbeat to complete. In addition to providing helpful visual feedback, research has demonstrated that acknowledging a slight delay will result in the user perceiving the delay as being shorter than they would if you did not acknowledge the delay. This is likely because we've been trained by good UI design to understand that this convention means we're waiting on the system. A sluggish UI otherwise forces people to wonder if they have done something wrong, and you don't want that.

javascript
beforeReflex () {
    document.body.classList.add('wait')
  }

  afterReflex () {
    document.body.classList.remove('wait')
  }
css
body.wait, body.wait * {
  cursor: wait !important;
}

Autofocus text boxes

If you are working with input elements in your application, you will quickly realize an unfortunate quirk of web browsers is that the autofocus attribute is only processed on the initial page load. If you want to implement a "click to edit" UI, you need to use a life-cycle callback method to make sure that the focus lands in the right place.

Handling this problem for every action would be extremely tedious. Luckily we can make use of the afterReflex callback to inspect the element to see if it has the autofocus attribute and, if so, correctly set the focus on that element.

javascript
afterReflex () {
    const focusElement = this.element.querySelector('[autofocus]')
    if (focusElement) {
      focusElement.focus()

      // shenanigans to ensure that the cursor is placed at the end of the existing value
      const value = focusElement.value
      focusElement.value = ''
      focusElement.value = value
    }
  }

TIP

Note that to obtain our focusElement, we looked for a single instance of autofocus on an element that is a child of our controller. We used this.element where this is a reference to the Stimulus controller.

If we wanted to only check the element that triggered the Reflex action, we would modify our afterReflex() to afterReflex(element) and then call element.querySelector - or just check the attributes directly.

If we wanted to check the whole page for an autofocus attribute, we can just use document.querySelector('[autofocus]') as usual. The square-bracket notation just tells your browser to look for an attribute called autofocus, regardless of whether it has a value or not.

Offering visual feedback

We recommend Velocity for light, tweening animations that alert the user to UI state changes.

You can see Velocity in action on the StimulusReflex Expo Todos demo.

Capture all DOM update events

Stimulus provides a really powerful event routing syntax that includes custom events, specifying multiple events and capturing events on document and window.

html
<div data-action="cable-ready:after-morph@document->chat#scroll">

By capturing the cable-ready:after-morph event, we can run code after every update from the server. In this example, the scroll method on our Chat controller is being called to scroll the content window to the bottom, displaying new messages.

Capture jQuery events with DOM event listeners

Don't hate jQuery: it was a life-saver 12 years ago, and many of its best ideas are now part of the JavaScript language. However, one of the uglier realities of jQuery in a contemporary context is that it has its own entirely proprietary system for managing events, and it's not compatible with the now-standard DOM events API.

Sometimes you still need to be able to interface with legacy components, but you don't want to have to write two event handling systems.

jquery-events-to-dom-events is an npm package that lets you easily access and respond to jQuery events.

Access Stimulus controller instances

Stimulus doesn't provide an easy way to access a controller instance; you have to have access to your Stimulus application object, the element, the name of the controller and be willing to call an undocumented API.

javascript
this.application.getControllerForElementAndIdentifier(
  document.getElementById('users'),
  'users'
)

This is ugly, verbose and potentially impossible outside of another Stimulus controller. Wouldn't it be nice to access your controller's methods and local variables from a legacy jQuery component? Just add this line to the initialize() method of your Stimulus controllers:

javascript
this.element[this.identifier] = this

This creates a document-scoped variable with the same name as your controller (or controllers!) on the element itself, so you can now call element.controllerName.method() without any Pilates required. You can read more about this technique here.

TIP

If your controller's identifier doesn't obey the rules of JavaScript variable naming conventions, you will need to specify a viable name for your instance.

For example, if your controller is named list-item you might consider this.element.listItem = this for that controller**.**

Server Side

Russian Doll caching

Caching is the secret to getting your application responding in 30-50ms after a database query. Some developers are intimidated by application-level caching, but you can ease into it.

You might be surprised how easy it can be to stash frequently accessed resources that are expensive to generate. This is known as a fragment cache. In this contrived example, the cached block will be expired and replaced if the current user or the todo is changed:

html
<% todo = Todo.first %>

<% cache([current_user, todo]) do %>
  ... a whole lot of work here ...
<% end %>

Russian Doll caching is just stacking cache fragments inside each other, and then configuring your ActiveRecord model callbacks to expire any keys that they are cached in when updated by setting the touch: true option on your belongs_to associations.

erb
<% cache([current_user, "todo_list", @todos.map(&:id), @todos.maximum(:updated_at)]) do %>
  <ul>
    <% @todos.each do |todo| %>
      <% cache(todo) do %>
        <li class="todo"><%= todo.description %></li>
      <% end %>
    <% end %>
  </ul>
<% end %>

Nate Berkopec's excellent post "The Complete Guide to Rails Caching" is one of the best resources on the topic - and the source of the above examples. It's a half-hour incredibly well-spent.

Speed up page morphs

Depending on what parts of your DOM are being morphed, it's possible that you don't need to render your layout template every time you run a Reflex. If your menus and sidebar are mostly static, you might want to experiment with constraining your update to just the template for the current action.

First, check to see if the current controller action is executing inside of a Reflex:

ruby
if @stimulus_reflex
  render layout: false
end

Then make sure that you're setting a data-reflex-root attribute containing a CSS selector that points to same DOM element where your template begins:

html
<div id="pow" data-reflex-root="#pow" data-reflex="click->Biff#pow">
  Pow.
</div>

Otherwise StimulusReflex will look for the body tag and not know what to do.

You can read more about scoping Page Morphs:

Internationalization

If you're building an application for an international audience, you might want to your Reflex class to be aware of the current user's location. It's important to remember that ActionCable Connections exist in their own context and will have the I18n.default_locale unless you change it.

You can set your I18n.locale in a before_reflex callback in your ApplicationReflex:

ruby
class ApplicationReflex < StimulusReflex::Reflex
  before_reflex do
    I18n.locale = :fr
  end
end

You might also want to set locale in a more granular way:

ruby
class ApplicationReflex < StimulusReflex::Reflex
  def with_locale
    I18n.with_locale(:de) { yield }
  end
end

Now you can wrap your render calls in your new with_locale helper:

ruby
class ExampleReflex < ApplicationReflex
  def foo
    morph "#foo", with_locale { render(partial: "path/to/foo") }
  end
end

Many developers will want to set the locale to a dynamic value from the session object:

ruby
class ApplicationReflex < StimulusReflex::Reflex
  before_reflex do
    I18n.locale = session[:locale]
  end
end

This requires that you set the session variable in your ApplicationController, as described at length in the Rails Internationalization Guide:

ruby
class ApplicationController < ActionController::Base
  around_action :switch_locale
  def switch_locale(&action)
    locale = params[:locale] || I18n.default_locale
    I18n.with_locale(locale, &action)
  end
end

It's also common to store a locale as a user preference:

ruby
class ApplicationReflex < StimulusReflex::Reflex
  delegate :current_user, to: :connection

  before_reflex do
    I18n.locale = current_user ? current_user.locale : I18n.default_locale
  end
end

If your ActionCable Connection could be established for users before they authenticate, make sure your logic can handle a nil value for current_user.

WARNING

Remember: if you're using Turbo Drive/Turbolinks, your ActionCable Connection will likely live across many page navigation events. ActionCable cannot access new session data until the Connection is reconnected. This usually happens with a hard page refresh, or you can terminate the Connection manually.

Pro-tip

If you're working on translations and would like to have your .yml files automatically reload when the browser refreshes, we've got you covered:

ruby
class ApplicationController < ActionController::Base
  before_action -> { I18n.backend.reload! } if Rails.env.development?
end

The Current pattern

Several years ago, DHH introduced the Current pattern to Rails 5.1. It's easy to work with Current objects inside of your Reflex classes using a before_reflex callback in your ApplicationReflex.

ruby
class ApplicationReflex < StimulusReflex::Reflex
  delegate :current_user, to: :connection

  before_reflex do
    Current.user = current_user
  end
end

The Current.user accessor is now available in your Reflex action methods.

ruby
class UserReflex < ApplicationReflex
  def follow
    user = User.find(element.dataset.user_id)
    Current.user.follow(user)

    morph "#following", render(
      partial: "users/following",
      locals: { user: Current.user }
    )
  end
end

You can also set the Current object in the connect method of your Connection module. You can see this approach in the tenant branch of the stimulus_reflex_harness app.

Adding log tags

TIP

Since StimulusReflex v3.4, there is now a vastly superior path in the form of StimulusReflex Logging. This section will be removed when the next release arrives.

You can prepend the id of the current User on messages logged from your Connection module.

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

    def connect
      self.current_user = env["warden"].user
      logger.add_tags "ActionCable: User #{current_user.id}"
    end

  end
end

Generating ids with dom_id

CableReady - which is included and available for use in your Reflex classes - exposes a variation of the dom_id helper found in Rails. It has the exact same function signature and behavior, with one subtle but important difference: it prepends a # character to the beginning of the generated id. Where the original function was intended for use in ActionView ERB templates, that # makes it perfect for use on the server, where the # character is required to refer to a DOM element id attribute.

ruby
class UserReflex < ApplicationReflex
  def profile
    user = User.find(element.dataset.user_id)
    morph dom_id(user), render(
      partial: "users/profile",
      locals: { user: user }
    )
  end
end

ViewComponentReflex

We're big fans of using ViewComponents in our template rendering process. The view_component_reflex gem offers a mechanism for persistent state in your ViewComponents by automatically storing your component state in the Rails session.

Rendering views inside of an ActiveRecord model or ActiveJob class

If you plan to initiate a CableReady broadcast inside of a model callback or job, you might find yourself trying to render templates and wondering why it seems to return nil.

The secret to an efficient and successful template render operation is to call the render method of the ApplicationController class.

ruby
class Notification < ApplicationRecord
  include CableReady::Broadcaster
  after_save do
    html = ApplicationController.render(
      partial: "layouts/navbar/notification",
      locals: { notification: self }
    )
    cable_ready["notification_feed:#{self.recipient.id}"]
      .insert_adjacent_html(
        selector: "#notification_dropdown",
        position: "afterbegin",
        html: html
      ).broadcast
  end
end

Flash messages

One Rails mechanism that you might use less in a StimulusReflex application is the flash message object. Flash made a lot more sense in the era of submitting a CRUD form and seeing the result confirmed on the next page load. With StimulusReflex, the current state of the UI might be updated dozens of times in rapid succession and the flash message could be easily lost before it's read.

You'll want to experiment with other, more contemporary feedback mechanisms to provide field validations and event notification functionality. An example would be the Facebook notification widget, or a dedicated notification drop-down that is part of your site navigation.

Clever use of CableReady broadcasts when ActiveJobs complete or models update is likely to produce a cleaner reactive surface for status information.

With all of those caveats established, Flash isn't dead, yet! Nate Hopkins has shared his pattern for working with Rails Flash in a StimulusReflex project.

You can access the ActionDispatch::Flash::FlashHash for the current request via the flash accessor inside of a Reflex action.

Use webpack-dev-server to reload after Reflex changes

It can be a pain to remember to reload your page after you make changes to a Reflex. Luckily, if you're already running bin/webpack-dev-server while you are building your application, you can add folders in your app to the list of places that are being monitored.

javascript
var path = require('path')
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')
environment.config.devServer.watchContentBase = true
environment.config.devServer.contentBase = [
  path.join(__dirname, '../../app/views'),
  path.join(__dirname, '../../app/helpers'),
  path.join(__dirname, '../../app/reflexes')
]

module.exports = environment.toWebpackConfig()

Released under the MIT License.