Building a Live Search Experience with StimulusReflex and Ruby on Rails28 Aug 2021
As we approach the release of Rails 7, the Rails ecosystem is full of options to build modern web applications, fast. Over the last 9 months, I’ve written articles on building type-as-you-search interfaces with Stimulus and with the full Hotwire stack, exploring a few of the options available to Rails developers.
Today, we’re going to build a live search experience once more. This time with StimulusReflex, a “new way to craft modern, reactive web interface with Ruby on Rails”. StimulusReflex relies on WebSockets to pass events from the browser to Rails, and back again, and uses morphdom to make efficient updates on the client-side.
When we’re finished, our application will look like this:
It won’t win any beauty contests, but it will give us a chance to explore a few of the core concepts of StimulusReflex.
As we work, you will notice some conceptual similarities between StimulusReflex and Turbo Streams, but there are major differences between the two projects, and StimulusReflex brings functionality and options that don’t exist in Turbo.
Before we get started, this article will be most useful for folks who are comfortable with Ruby on Rails and who are new to StimulusReflex. If you prefer to skip ahead to the source for the finished project, you can find the full code that accompanies this article on Github.
Let’s dive in.
To get started, we’ll create a new Rails application, install StimulusReflex, and scaffold up a
Player resource that users will be able to search.
From your terminal:
In addition to the above, you’ll also need to have Redis installed and running in your development environment.
stimulus_reflex:install task will be enough to get things working in development but you should review the installation documentation in detail ahead of any production deployment of a StimulusReflex application.
With the core of the application ready to go, start up your rails server and head to http://localhost:3000/players.
Create a few players in the UI or from the Rails console before moving on.
Search, with just Rails
We’ll start by adding the ability to search players without any StimulusReflex at all, just a normal search form that hits the existing
To start, update
players/index.html.erb as shown below:
Here we’re rendering a search form at the top of the page and we’ve moved rendering players to a partial that doesn’t exist yet.
Create that partial next:
And fill it in with:
Finally, we’ll add a very rudimentary search implementation to the
index method in the
PlayersController, like this:
If you refresh /players now, you should be able to type in a search term, submit the search request to the server, and see the search applied when the index page reloads.
Now let’s start adding StimulusReflex, one layer at a time.
Creating a reflex
StimulusReflex is built around the concept of reflexes. A
reflex, to oversimplify it a bit, is a Ruby class that responds to user interactions from the front end.
Reflexes can do a lot, from re-rendering an entire page on demand to kicking off background jobs, but for our purposes we’re going to create one reflex that handles user interactions with the search form we added in the last section.
Instead of a GET request to the
players#index action, form submissions will call a method in the reflex class that processes the search request and updates the DOM with the results of the search.
We’ll start with generating the reflex using the built-in generator:
This generator creates two files, a
PlayerSearch reflex class in the
reflexes directory and a
player-search Stimulus controller in
We’ll define the reflex in the
PlayerSearch class, and then, optionally, we can use the
player-search controller to trigger that reflex from a front end action and hook into reflex lifecycle methods that we might care about on the front end.
The simplest implementation of a working
PlayerSearch reflex is to update an instance variable from the reflex, and then rely on a bit of StimulusReflex magic to do everything else. We’ll start with the magical version.
First, add a
search method to the
Then update the search form to trigger the reflex on submit:
PlayersController#index to only assign a new value to
@players when it hasn’t already been set by the reflex action.
With these changes in place, we can refresh the players page, submit a search, and see that searching works fine. So what’s going on here?
In the form, we’re listening for the submit event and, when it is triggered, the
data-reflex attribute fires the
search method that we defined in the
PlayerSearch reflex class.
PlayerSearch.search automatically gets access the
params from the nearest form so we can use
params[:query] like we would in a controller action.
We use the query param to assign a value to
@players and, because we haven’t told it to do anything else, the reflex finishes by processing
PlayersController#index, passing the updated
players instance variable along the way and using
morphdom to update the content of the page as efficiently as possible.
So we can finish this article by deleting the form’s submit button and moving the reflex from the submit event on the form to the input event on the text field, right?
Not so fast.
While what we have “works”, our implementation is currently inefficient and hard to maintain and expand. Future developers will have to piece together what’s going on in
search. We’re also re-rendering the entire HTML body even though we know that only a small part of the page actually needs to change.
We can do a little better.
Using Selector Morphs
The magical re-processing of the
index action happens because the default behavior of a reflex is to trigger a full-page morph when a reflex method runs.
While page morphs are easy to work with, we can be more explicit about our intentions and more precise in our updates by using selector morphs.
Selector morphs are more efficient than page morphs because selector morphs skip routing, controller actions, and template rendering. Selector morphs are also more clear in their intention and easier to reason about since we know exactly what will change on the page when the reflex runs.
Full page morphs are powerful and simple to use, but my preference is to use selector morphs when the use case calls for updating small portions of the page.
Let’s replace the magical page morph with a selector morph.
First, as you might have guessed, selector morphs use an identifier to target their DOM changes. We’ll add an id to the
<tbody> in the
players partial to give the selector morph something to target.
Next we’ll update the search form:
Here we’ve scrapped the submit button and we’ve replaced the
data-reflex on the submit button with a Stimulus controller directly on the query text field.
player-search controller was created by the generator we ran earlier to create the
PlayerSearch reflex, and we’ll fill in the Stimulus controller next:
Here we’re inheriting from a Stimulus
ApplicationController, which was automatically created by the
stimulus_reflex:install task we ran at the beginning of this article. Since we’re inheriting from
ApplicationController, we have access to
this.stimulate, which we can use to trigger any reflex we like.
Why would we use a Stimulus controller instead of a
data-reflex attribute on a DOM element?
Using a Stimulus controller gives us a little more flexibility and power than if we attach the reflex to the DOM directly, which we’ll explore in the next section.
Before we expand the Stimulus controller, let’s finish up the implementation of the selector morph by updating the
PlayerSearch#search like this:
Here we no longer need
players to be an instance variable. Instead, we pass it in as a local to the players partial which the selector morph renders to replace the children of
With this in place, we can refresh the page and start typing in the search form. If you’ve followed along so far, you should see that as you type, the content of the players table is updated.
If you check the server logs, you’ll see that instead of the controller action processing and the entire application layout re-rendering, the server only runs the database query to filter the players and then renders the players partial. Skipping routing and full page rendering dramatically reduces the amount of time and resources used to handle the request.
Expanding the Stimulus controller
Now we’ve got live search in place using a selector morph. Incredible work so far!
Let’s finish up by expanding the Stimulus controller to make the user experience a bit cleaner and learn a little more about StimulusReflex in the process.
First, searching on each keystroke isn’t ideal. Let’s adjust
search to wait for the user to stop typing before calling the
Nothing fancy here, and you should probably consider a more battle tested debounce function in production, but it’ll do for today.
Next, it would be nice to give the user a visual cue that the list of players has updated. One way to do that is to animate the list when it updates and StimulusReflex helpfully gives us an easy way to listen for and react to reflex life-cycle events.
Here we’re combining a custom StimulusReflex client-side life-cycle callback (
beforeSearch) with the Web Animations API to add a simple fade effect to the players list each time it updates.
In addition to the client-side events, StimulusReflex provides server-side life-cycle callbacks, which we don’t have a use for in this particular article, but they exist if you need them.
Now we have visual feedback for users as they type. Let’s finish this article by allowing users to clear a search without having to backspace the input until its empty.
This last exercise will give us a chance to look at using more than one selector morph in a single reflex and to expand the Stimulus controller a bit more.
Resetting search results
Our goal is to add a link to the page that displays whenever the search text box isn’t empty. When a user clicks the link, the search box should be cleared, the players list should be updated to list all of the players in the database, and the reset link should be hidden.
We’ll start by adding a new partial to render the link:
And fill that in with:
The reset link will only display if the local
query variable is present. Clicks on the link are routed to a
player-search Stimulus controller, calling the
reset function that doesn’t exist yet.
Before we update the Stimulus controller, let’s adjust the index view, like this:
Here we’ve inserted the a new
reset_link partial, wrapped in a
More importantly, we’ve adjusted how the
player-search Stimulus controller is connected to the DOM. Instead of the controller being attached to the search text field, the controller is now on a wrapper div.
While we didn’t have to make this change to the controller connection, doing so makes it clear that the controller is interested in more than just the text input and opens up the possibility of using targets to more specifically reference DOM elements in the future.
This change also gives us an opportunity to look at one more piece of functionality of StimulusReflex-enabled functionality in Stimulus controllers.
Update the Stimulus controller like this:
We’ve made two important changes here.
First, since the Stimulus controller is no longer inside of the search form, the search reflex will no longer be able to reference
params implicitly. We handle this change by passing the value of the search box to
stimulate as an additional argument.
Stimulate “is extremely flexible” and we take advantage of that flexibility to ensure the search reflex receives the search query even without access to the search form’s params.
Next, we added
reset, which simply triggers the
search reflex without an additional argument.
On the server side, we need to update
PlayerSearch#search like this:
Here we updated
search to take an optional
query argument. The value of
query is used to set the value of
players and then two selector morphs replace the content of
In action, our final product looks like this:
An alternative approach
If you review the method signature of
stimuluate, you’ll notice that we could have solved the problem of passing the value of the search box to the server in other ways.
Instead of passing in event.target.value, we could have passed
event.target like this:
This approach would override the default value of the server-side Reflex
element, allowing us to call
event.target.value to access the value of the search box from the server.
While this would work for the
search function, it wouldn’t work for
reset since we need to ignore the value of the search box when resetting the form. We could make it all work by passing an element to override the default
element assignment, but it would take more effort.
Passing in the value explicitly allows us to use
PlayerSearch#search to handle both
reset requests and keeps our code a bit cleaner on the server side.
This is a matter of preference without a definitive answer on which approach is “best”. Implementing a solution overriding
element on the server side would work fine. Also viable would be using an entirely different reflex action for the reset link.
StimulusReflex offers plenty of flexibility, and some choices will come down to what feels best to you and your team.
Today we looked at implementing a simple search-as-you-type interface with Ruby on Rails and StimulusReflex. This simple example should give you some indication of the power StimulusReflex has to deliver modern, fast web applications while keeping code complexity low and developer happiness high.
Even better, StimulusReflex plays nicely with Turbo Drive and Turbo Frames, giving developers the ability to mix-and-match to choose the best tool for the job.
To keep learning about building Rails applications with StimulusReflex:
- Dive into the (excellent, very well-maintained) official documentation
- Check out demo applications demonstrating some core StimulusReflex concepts
- Join the StimulusReflex discord to learn from lots of folks way sharper than me
- Learn more advanced usage patterns with StimulusReflexPatterns from Julian Rubisch
As always, thanks for reading!