Useful Patterns

How to build a great StimulusReflex application

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.

That said, if you're building applications with StimulusReflex, you're going to want to bookmark this page. If you discover useful patterns not documented here, please open an issue or submit a pull request.

Client Side

Application Controller

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 lifecycle callback methods for your entire application.

application_controller.js
application_controller.js
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.')
}
}

Then, all that's required to create a StimulusReflex controller is inherit from ApplicationController:

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

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

Benchmarking your Reflex actions

You might want to see how long your Reflex actions are taking to complete a round-trip, and without Ajax calls to monitor getting reliable metrics requires new approaches.

We suggest making use of the beforeReflex and afterReflex lifecycle callback methods to sample your performance. As a rule of thumb, anything below 200-300ms will be perceived as "native" by your users.

You can add this code to your desired Reflex controller. If you're making use of the Application Controller pattern described above, all of your Reflexes will log their round-trip execution times.

application_controller.js
application_controller.js
beforeReflex () {
this.benchmark = performance.now()
}
afterReflex (element, reflex) {
console.log(reflex, `${(performance.now() - this.benchmark).toFixed(0)}ms`)
}

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.

application_controller.js
application.css
application_controller.js
beforeReflex () {
document.body.classList.add('wait')
}
afterReflex () {
document.body.classList.remove('wait')
}
application.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 lifecycle 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.

application_controller.js
application_controller.js
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
}
}

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.

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.

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

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.

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:

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.

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

Chained Reflexes for long-running actions

Ideally, you want your Reflex action methods to be as fast as possible. Otherwise, no amount of client-side magic will cover for the fact that your app feels slow. If your round-trip click-to-redraw time is taking more than 300ms, people will describe the experience as sluggish. We can optimize our queries, make use of Russian Doll caching, and employ many other performance tricks in the app... but what if we rely on external, 3rd party services? Some tasks just take time, and for those situations, we wait for it:

example_reflex.rb
example_reflex.rb
def wait_for_it(target)
if block_given?
Thread.new do
@channel.receive({
"target" => "#{self.class}##{target}",
"args" => [yield],
"url" => url,
"attrs" => element.attributes.to_s,
"selectors" => selectors,
})
end
end
end

This is code that you can insert into the bottom of your Reflex classes. It might look a bit arcane, but it allows our Reflex action methods to call other Reflex actions after their work is complete.

Let's explore this with a contrived example. When the page first loads, we see a button. If you click the button, it hides the button and displays a "Waiting" message while the server calls a slow API. When the API call comes back, it updates the page with the result.

index.html.erb
index.html.erb
<div data-controller="example">
<% case @api_status %>
<% when :loading %>
<em>Waiting...</em>
<% when :ready %>
<strong>@api_response</strong>
<% else %>
<button data-reflex="click->ExampleReflex#api">Call API</button>
<% end %>
</div>

As you can see, we're following all of the proper StimulusReflex naming conventions; we have defined a simple component that has an example Stimulus controller defined. The button declares that it will call the api Reflex action of our ExampleReflex class on the server.

Since there is no @api_status instance variable during the initial page load, the case statement defaults to the else case which is our initial view state. Unfortunately, this might look visually confusing at first.

If you really want to ditch the else you can define an initial state in your Rails controller:

def index
@api_status ||= :default
end

Now, you can refactor your view template like this:

<div data-controller="example">
<% case @api_status %>
<% when :default %>
<button data-reflex="click->ExampleReflex#api">Call API</button>
<% when :loading %>
<em>Waiting...</em>
<% when :ready %>
<strong>@api_response</strong>
<% end %>
</div>

We've got your back.

Now, let's revisit our ExampleReflex class. When the user clicks the button, it calls our api action. The @api_status is set to :loading and wait_for_it gets called specifying the success action as the callback. Since wait_for_it operates asyncronously in its own thread, the action immediately sends the template back to the client to notify them that a slow process has started.

example_reflex.rb
example_reflex.rb
def api
@api_status = :loading
wait_for_it(:success) do
sleep 3 # DON'T EVER ACTUALLY DO THIS IRL
"Worth the wait!"
end
end
def success(response)
@api_status = :ready
@api_response = response
end
private
def wait_for_it(target)
return unless respond_to? target
if block_given?
Thread.new do
channel.receive({
"target" => "#{self.class}##{target}",
"args" => [yield],
"url" => url,
"attrs" => element.attributes.to_h,
"selectors" => selectors,
})
end
end
end

As you can see, we're only pretending to call an API for this example. Do not call sleep in a production Ruby web application. sleep tells your web server to stop dreaming of new possibilities. However, assuming that you're the only person on your development machine, you'll see that after three seconds, a second Reflex action is triggered and delivered to the browser over the websocket connection.

This is one of the coolest things about websockets; you can respond many times to a single request, or not at all. It's an entirely different mental model than Ajax and HTTP.

Triggering custom events and forcing DOM updates

CableReady, one of StimulusReflex's dependencies, has many handy methods that you can call from controllers, ActionJob tasks and Reflex classes. One of those methods is dispatch_event, which allows you to trigger any event in the client, including custom events and jQuery events.

In this example, we send out an event to everyone connected to ActionCable suggesting that update is required:

Ruby
Ruby
class NotificationReflex < StimulusReflex::Reflex
include CableReady::Broadcaster
def force_update(id)
cable_ready["StimulusReflex::Channel"].dispatch_event {
name: "force:update",
detail: {id: id},
}
cable_ready.broadcast
end
def reload
# noop: this method exists so we can refresh the DOM
end
end
index.html.erb
index.html.erb
<div data-action="force:update@document->notification#reload">
<button data-action="notification#forceUpdate">
</div>

We use the Stimulus event mapper to call our controller's reload method whenever a force:update event is received:

notification_controller.js
notification_controller.js
let lastId
export default class extends Controller {
forceUpdate () {
lastId = Math.random()
this.stimulate("NotificationReflex#force_update", lastId)
}
reload (event) {
const { id } = event.detail
if (id === lastId) return
this.stimulate("NotificationReflex#reload")
}
}

By passing a randomized number to the Reflex as an argument, we allow ourselves to return before triggering a reload if we were the ones that initiated the operation.

Coming Soon: Notifications with ActiveJob / Sidekiq