← All posts

Replacing jQuery with Stimulus

September 18, 2024 rails stimulus

If your Rails app still has jQuery and you’re thinking about migrating to Stimulus, here’s what the process actually looks like. I’ve done this on a few projects now and it’s more mechanical than you’d expect.

Set Up Stimulus

If you’re on Rails 7+, Stimulus is already wired up via importmap-rails or your bundler. For older apps, add the stimulus-rails gem and run bin/rails stimulus:install. You’ll get a app/javascript/controllers/ directory and an auto-loader.

Map jQuery Patterns to Controllers

Most jQuery code falls into a few repeating patterns. Each one maps directly to a Stimulus controller.

Toggle visibility or classes:

// jQuery
$('.toggle-btn').on('click', function() {
  $(this).next('.content').toggleClass('hidden');
});
// Stimulus controller (toggle_controller.js)
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["content"]

  toggle() {
    this.contentTarget.classList.toggle("hidden")
  }
}
<div data-controller="toggle">
  <button data-action="toggle#toggle">Show/Hide</button>
  <div data-toggle-target="content">...</div>
</div>

AJAX form submissions — remove the jQuery $.ajax call and let Turbo handle the form natively. If you need custom behavior on success/failure, use data-action="turbo:submit-end->controller#method".

Tabbed interfaces, accordions, modals — same idea. One small controller per behavior, targets in the markup, actions on the elements that trigger them.

Replace Selectors with Targets

This is the most tedious part. jQuery grabs elements by class or ID from anywhere. Stimulus wants you to declare targets explicitly with data-*-target attributes in the HTML. More markup, but the connection between HTML and JavaScript is visible instead of buried in a selector string.

Go file by file. For each jQuery block, identify what elements it touches, add the data-controller and data-*-target attributes, and move the logic into a controller.

Drop jQuery

Once you’ve migrated all the handlers, remove jquery and jquery-ujs (or jquery_ujs) from your bundle. If you have jQuery plugins, check whether you actually still need them — most were solving browser inconsistencies that don’t exist anymore. $.ajax is fetch. Animations are CSS transitions.

What to Watch For

  • Document-ready timing: Stimulus controllers connect automatically when their element hits the DOM, so you don’t need $(document).ready. But if you have code that runs on page load outside of event handlers, move it to a connect() method on a controller.
  • Event delegation: jQuery’s .on('click', '.child', fn) delegates events. Stimulus handles this naturally since actions are declared on the elements themselves.
  • Third-party plugins: If a jQuery plugin doesn’t have a vanilla JS alternative, wrap it in a Stimulus controller and keep jQuery loaded only for that. Migrate it last.

The whole migration on a medium-sized app usually takes a few days. The result is about 70 KB less JavaScript and code that new developers can trace from the HTML instead of grepping through a monolithic JS file.

Let’s work together! Tell Me About Your Project