Building a Live Search Experience with StimulusReflex and Ruby on Rails
Tags: rails, ruby, stimulusreflex, webdev
Published: 2021-08-28 20:33:34

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](https://www.colby.so/posts/live-search-with-stimulus-and-rails-6) and with [the full Hotwire stack](https://www.colby.so/posts/instant-search-with-rails-6-and-hotwire), 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](https://github.com/patrick-steele-idem/morphdom) to make efficient updates on the client-side.

When we’re finished, our application will look like this:

![A screen recording of a user on a web page with a list of player names below a search text box. The user types 'Dirk' in the search box and the list of player names is automatically filtered based on the search term. The user clicks a clear search link and the list of players resets while the text box is cleared.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6yhm3y25hmoiditndneh.gif)

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](https://github.com/DavidColby/stimulus-reflex-search).

Let’s dive in.

## Setup
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:

```shell
rails new stimulus-reflex-search -T
cd stimulus-reflex-search
bundle add stimulus_reflex
bundle exec rails stimulus_reflex:install
rails g scaffold Players name:string
rails db:migrate
```

In addition to the above, you’ll also need to have Redis installed and running in your development environment.

The `stimulus_reflex:install` task will be enough to get things working in development but you should review the [installation documentation](https://docs.stimulusreflex.com/hello-world/setup) 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](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 `players#index` action.

To start, update `players/index.html.erb` as shown below:

```erb
<p id="notice"><%= notice %></p>

<h1>Players</h1>
<%= form_with(url: players_path, method: :get) do |form| %>
  <%= form.label :query, "Search by name:" %>
  <%= form.text_field :query, value: params[:query] %>
  <%= form.submit 'Search', name: nil %>
<% end %>
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <%= render "players", players: @players %>
</table>

<br>

<%= link_to 'New Player', new_player_path %>
```

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:

```
touch app/views/players/_players.html.erb
```

And fill it in with:

```erb
<tbody>
  <% players.each do |player| %>
    <tr>
      <td><%= player.name %></td>
      <td><%= link_to 'Show', player %></td>
      <td><%= link_to 'Edit', edit_player_path(player) %></td>
      <td><%= link_to 'Destroy', player, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
</tbody>
```

Finally, we’ll add a very rudimentary search implementation to the `index` method in the `PlayersController`, like this:

```ruby
def index
  @players = Player.where("name LIKE ?", "%#{params[:query]}%")
end
```

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.

When you’re working with StimulusReflex, you’ll write a lot of reflexes that do work that might otherwise require controller actions and a lot of client-side JavaScript logic.

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:

```
rails g stimulus_reflex PlayerSearch
```

This generator creates two files, a `PlayerSearch` reflex class in the `reflexes` directory and a `player-search` Stimulus controller in `javascripts/controllers`.

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 `PlayerSearch` reflex:

```ruby
def search
  @players = Player.where('name LIKE ?', "%#{params[:query]}%")
end
```

Then update the search form to trigger the reflex on submit:

```erb
<%= form_with(method: :get, data: { reflex: "submit->PlayerSearch#search" }) do |form| %>
  <%= form.label :query, "Search by name:" %>
  <%= form.text_field :query, value: params[:query] %>
  <%= form.submit 'Search', name: nil %>
<% end %>
```

And update `PlayersController#index` to only assign a new value to `@players` when it hasn't already been set by the reflex action.

```ruby
def index
  @players ||= Player.all
end
```

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](https://docs.stimulusreflex.com/rtfm/morph-modes#page-morphs) 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](https://docs.stimulusreflex.com/rtfm/morph-modes#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.

```html
<tbody id="players-list">
<!-- Snip -->
</tbody>
```

Next we’ll update the search form:

```erb
<%= form_with(url: players_path, method: :get) do |form| %>
  <%= form.label :query, "Search by name:" %>
  <%= form.text_field :query, value: params[:query], data: { controller: "player-search", action: "input->player-search#search" }  %>
<% end %>
```

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.

The `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:

```javascript
import ApplicationController from './application_controller'

export default class extends ApplicationController {
  search() {
    this.stimulate('PlayerSearch#search')
  }
}
```

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:

```ruby
def search
  players = Player.where('name LIKE ?', "%#{params[:query]}%")
  morph '#players-list', render(partial: 'players/players', locals: { players: players })
end
```

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 `#players-list`.

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 `PlayerSearch` reflex.

```javascript
search() {
  clearTimeout(this.timeout)
  this.timeout = setTimeout(() => {
    this.stimulate('PlayerSearch#search')
  }, 200)
}
```

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.

```javascript
beforeSearch() {
  this.playerList.animate(
    this.fadeIn,
    this.fadeInTiming
  )
}

get fadeIn() {
  return [
    { opacity: 0 },
    { opacity: 1 }
  ]
}

get fadeInTiming() {
  return { duration: 300 }
}

get playerList() {
  return document.getElementById('players-list')
}
```

Here we’re combining a custom StimulusReflex [client-side life-cycle](https://docs.stimulusreflex.com/rtfm/lifecycle#custom-life-cycle-methods) callback (`beforeSearch`) with the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/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](https://docs.stimulusreflex.com/rtfm/lifecycle#server-side-reflex-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:

```
touch app/views/players/_reset_link.html.erb
```

And fill that in with:

```erb
<% if query.present? %>
  <a href="#" data-action="click->player-search#reset">Clear search</a>
<% end %>
```

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:

```erb
<p id="notice"><%= notice %></p>
<h1>Players</h1>
<div data-controller="player-search">
  <%= form_with(method: :get) do |form| %>
    <%= form.label :query, "Search by name:" %>
    <%= form.text_field :query, data: { action: "input->player-search#search" }, autocomplete: "off", value: params[:query] %>
  <% end %>
  <div id="reset-link">
    <%= render "reset_link", query: params[:query] %>
  </div>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th colspan="3"></th>
      </tr>
    </thead>
    <%= render "players", players: @players %>
  </table>
</div>
<br>

<%= link_to 'New Player', new_player_path %>
```

Here we’ve inserted the a new `reset_link` partial, wrapped in a `#reset-link` div.

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:

```javascript
search(event) {
  clearTimeout(this.timeout)
  this.timeout = setTimeout(() => {
    this.stimulate('PlayerSearch#search', event.target.value)
  }, 200)
}

reset(event) {
  event.preventDefault()
  this.stimulate('PlayerSearch#search')
}
```

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](https://docs.stimulusreflex.com/rtfm/reflexes#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:

```ruby
def search(query = '')
  players = Player.where('name LIKE ?', "%#{query}%")
  morph '#players-list', render(partial: 'players/players', locals: { players: players })
  morph '#reset-link', render(partial: 'players/reset_link', locals: { query: query })
end
```

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 `players-list` and `reset-link`.

In action, our final product looks like this:

![A screen recording of a user on a web page with a list of player names below a search text box. The user types 'Dirk' in the search box and the list of player names is automatically filtered based on the search term. The user clicks a clear search link and the list of players resets while the text box is cleared.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6i394fb7udndn8hvo8o4.gif)

## 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.stimulate(‘PlayerSearch#search, event.target)`.

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 `search` and `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.

## Wrapping up
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](https://docs.stimulusreflex.com/)
* Check out [demo applications](http://expo.stimulusreflex.com/) demonstrating some core StimulusReflex concepts
* Join the [StimulusReflex discord](https://discord.gg/stimulus-reflex) to learn from lots of folks way sharper than me
* Learn more advanced usage patterns with [StimulusReflexPatterns](https://www.stimulusreflexpatterns.com/patterns/) from [Julian Rubisch](https://twitter.com/julian_rubisch)
* (Shameless plug alert) Sign up for [Hotwiring Rails](https://landing.mailerlite.com/webforms/landing/d7z0n0), my monthly newsletter on the latest developments in Rails-land as we prepare for the launch of Rails 7

As always, if you have questions or feedback about this article, you can comment here, or you can find me on [Twitter](https://twitter.com/davidcolbyatx).

Thanks for reading!